|
4 | 4 | "fmt" |
5 | 5 | "os" |
6 | 6 |
|
| 7 | + "github.com/prabhakaran-jm/cilium-policypilot/internal/explain" |
7 | 8 | "github.com/prabhakaran-jm/cilium-policypilot/internal/hubble" |
8 | 9 | "github.com/prabhakaran-jm/cilium-policypilot/internal/synth" |
9 | 10 | "github.com/prabhakaran-jm/cilium-policypilot/internal/validate" |
@@ -263,16 +264,105 @@ func cmdVerify() *cobra.Command { |
263 | 264 | } |
264 | 265 |
|
265 | 266 | func cmdExplain() *cobra.Command { |
266 | | - return &cobra.Command{ |
| 267 | + var flowsFile string |
| 268 | + var policiesFile string |
| 269 | + var outputFile string |
| 270 | + |
| 271 | + cmd := &cobra.Command{ |
267 | 272 | Use: "explain", |
268 | | - Short: "Generate simple HTML report", |
| 273 | + Short: "Generate HTML report with policy summary and network graph", |
| 274 | + Long: "Generate an HTML report with flow statistics, generated policies, and network visualization.", |
269 | 275 | RunE: func(cmd *cobra.Command, args []string) error { |
270 | | - fmt.Println("Explain: writing out/report.html") |
271 | | - if err := os.MkdirAll("out", 0o755); err != nil { |
272 | | - return fmt.Errorf("failed to create output directory: %w", err) |
| 276 | + // Set defaults |
| 277 | + if flowsFile == "" { |
| 278 | + flowsFile = "out/flows.json" |
| 279 | + } |
| 280 | + if policiesFile == "" { |
| 281 | + policiesFile = "out/policy.yaml" |
| 282 | + } |
| 283 | + if outputFile == "" { |
| 284 | + outputFile = "out/report.html" |
273 | 285 | } |
274 | | - html := "# PolicyPilot Report\n\n```mermaid\ngraph TD; frontend-->catalog;\n```\n" |
275 | | - return os.WriteFile("out/report.html", []byte(html), 0o644) |
| 286 | + |
| 287 | + // Validate input files |
| 288 | + if err := validate.FilePath(flowsFile); err != nil { |
| 289 | + return fmt.Errorf("invalid flows file: %w", err) |
| 290 | + } |
| 291 | + if err := validate.FileExtension(flowsFile, ".json"); err != nil { |
| 292 | + return fmt.Errorf("flows file must be JSON: %w", err) |
| 293 | + } |
| 294 | + |
| 295 | + // Validate output path |
| 296 | + if err := validate.OutputPath(outputFile); err != nil { |
| 297 | + return fmt.Errorf("invalid output path: %w", err) |
| 298 | + } |
| 299 | + if err := validate.FileExtension(outputFile, ".html"); err != nil { |
| 300 | + return fmt.Errorf("output file must be HTML: %w", err) |
| 301 | + } |
| 302 | + |
| 303 | + fmt.Printf("Reading flows from %s...\n", flowsFile) |
| 304 | + collection, err := hubble.ReadFlowsFromFile(flowsFile) |
| 305 | + if err != nil { |
| 306 | + return fmt.Errorf("failed to read flows: %w", err) |
| 307 | + } |
| 308 | + |
| 309 | + // Parse flows |
| 310 | + parsedFlows, err := hubble.ParseFlows(collection) |
| 311 | + if err != nil { |
| 312 | + return fmt.Errorf("failed to parse flows: %w", err) |
| 313 | + } |
| 314 | + |
| 315 | + if len(parsedFlows) == 0 { |
| 316 | + return fmt.Errorf("no valid flows found") |
| 317 | + } |
| 318 | + |
| 319 | + fmt.Printf("Found %d parsed flows\n", len(parsedFlows)) |
| 320 | + |
| 321 | + // Read policies if file exists |
| 322 | + var policies []*synth.Policy |
| 323 | + if _, err := os.Stat(policiesFile); err == nil { |
| 324 | + fmt.Printf("Reading policies from %s...\n", policiesFile) |
| 325 | + // For now, we'll synthesize policies from flows |
| 326 | + // In the future, we could parse the YAML file |
| 327 | + policies, err = synth.SynthesizePolicies(parsedFlows) |
| 328 | + if err != nil { |
| 329 | + return fmt.Errorf("failed to synthesize policies: %w", err) |
| 330 | + } |
| 331 | + fmt.Printf("Found %d policies\n", len(policies)) |
| 332 | + } else { |
| 333 | + // Generate policies from flows |
| 334 | + fmt.Println("No policy file found. Generating policies from flows...") |
| 335 | + policies, err = synth.SynthesizePolicies(parsedFlows) |
| 336 | + if err != nil { |
| 337 | + return fmt.Errorf("failed to synthesize policies: %w", err) |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + // Generate report |
| 342 | + fmt.Println("Generating report...") |
| 343 | + reportData, err := explain.GenerateReport(parsedFlows, policies) |
| 344 | + if err != nil { |
| 345 | + return fmt.Errorf("failed to generate report: %w", err) |
| 346 | + } |
| 347 | + |
| 348 | + // Write HTML report |
| 349 | + if err := explain.WriteHTMLReport(reportData, outputFile); err != nil { |
| 350 | + return fmt.Errorf("failed to write HTML report: %w", err) |
| 351 | + } |
| 352 | + |
| 353 | + fmt.Printf("Report saved to %s\n", outputFile) |
| 354 | + fmt.Printf(" - %d flows analyzed\n", reportData.FlowCount) |
| 355 | + fmt.Printf(" - %d policies generated\n", reportData.PolicyCount) |
| 356 | + fmt.Printf(" - %d namespaces\n", len(reportData.Namespaces)) |
| 357 | + fmt.Printf(" - Network graph included\n") |
| 358 | + |
| 359 | + return nil |
276 | 360 | }, |
277 | 361 | } |
| 362 | + |
| 363 | + cmd.Flags().StringVarP(&flowsFile, "flows", "f", "", "Input flows JSON file (default: out/flows.json)") |
| 364 | + cmd.Flags().StringVarP(&policiesFile, "policies", "p", "", "Input policies YAML file (default: out/policy.yaml)") |
| 365 | + cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output HTML report file (default: out/report.html)") |
| 366 | + |
| 367 | + return cmd |
278 | 368 | } |
0 commit comments