diff --git a/pkg/cmd/scan.go b/pkg/cmd/scan.go index cca47895..67ba3de8 100644 --- a/pkg/cmd/scan.go +++ b/pkg/cmd/scan.go @@ -61,12 +61,16 @@ func NewScanCmd() *cobra.Command { ) } - outputFlag, _ := cmd.Flags().GetString("output") - out, err := parseOutputFlag(outputFlag) + outputFlag, _ := cmd.Flags().GetStringSlice("output") + if len(outputFlag) == 0 { + outputFlag = append(outputFlag, output.Example(output.ConsoleOutputType)) + } + + out, err := parseOutputFlags(outputFlag) if err != nil { return err } - opts.Output = *out + opts.Output = out filterFlag, _ := cmd.Flags().GetStringArray("filter") @@ -117,10 +121,10 @@ func NewScanCmd() *cobra.Command { " - Type =='aws_s3_bucket && Id != 'my_bucket' (excludes s3 bucket 'my_bucket')\n"+ " - Attr.Tags.Terraform == 'true' (include only resources that have Tag Terraform equal to 'true')\n", ) - fl.StringP( + fl.StringSliceP( "output", "o", - output.Example(output.ConsoleOutputType), + []string{}, "Output format, by default it will write to the console\n"+ "Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n", ) @@ -190,7 +194,6 @@ func NewScanCmd() *cobra.Command { func scanRun(opts *pkg.ScanOptions) error { store := memstore.New() - selectedOutput := output.GetOutput(opts.Output, opts.Quiet) c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) @@ -256,9 +259,12 @@ func scanRun(opts *pkg.ScanOptions) error { analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String() analysis.ProviderName = resourceSchemaRepository.ProviderName - err = selectedOutput.Write(analysis) - if err != nil { - return err + for _, o := range opts.Output { + selectedOutput := output.GetOutput(o, opts.Quiet) + err = selectedOutput.Write(analysis) + if err != nil { + return err + } } globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second))) @@ -352,6 +358,18 @@ func parseFromFlag(from []string) ([]config.SupplierConfig, error) { return configs, nil } +func parseOutputFlags(out []string) ([]output.OutputConfig, error) { + result := make([]output.OutputConfig, 0, len(out)) + for _, v := range out { + o, err := parseOutputFlag(v) + if err != nil { + return result, err + } + result = append(result, *o) + } + return result, nil +} + func parseOutputFlag(out string) (*output.OutputConfig, error) { schemeOpts := strings.Split(out, "://") if len(schemeOpts) < 2 || schemeOpts[0] == "" { diff --git a/pkg/cmd/scan_test.go b/pkg/cmd/scan_test.go index 9a72985f..1825d625 100644 --- a/pkg/cmd/scan_test.go +++ b/pkg/cmd/scan_test.go @@ -48,6 +48,7 @@ func TestScanCmd_Valid(t *testing.T) { {args: []string{"scan", "--tf-provider-version", "3.30.2"}}, {args: []string{"scan", "--driftignore", "./path/to/driftignore.s3"}}, {args: []string{"scan", "--driftignore", ".driftignore"}}, + {args: []string{"scan", "-o", "html://result.html", "-o", "json://result.json"}}, } for _, tt := range cases { @@ -164,98 +165,140 @@ func Test_parseFromFlag(t *testing.T) { func Test_parseOutputFlag(t *testing.T) { type args struct { - out string + out []string } tests := []struct { name string args args - want *output.OutputConfig + want []output.OutputConfig err error }{ { - name: "test empty", + name: "test empty output", args: args{ - out: "", + out: []string{""}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Unable to parse output flag '': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"), }, + { + name: "test empty array", + args: args{ + out: []string{}, + }, + want: []output.OutputConfig{}, + err: nil, + }, { name: "test invalid", args: args{ - out: "sdgjsdgjsdg", + out: []string{"sdgjsdgjsdg"}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Unable to parse output flag 'sdgjsdgjsdg': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"), }, { name: "test invalid", args: args{ - out: "://", + out: []string{"://"}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Unable to parse output flag '://': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"), }, { name: "test unsupported", args: args{ - out: "foobar://", + out: []string{"foobar://"}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Unsupported output 'foobar': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"), }, { name: "test empty json", args: args{ - out: "json://", + out: []string{"json://"}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Invalid json output 'json://': \nMust be of kind: json://PATH/TO/FILE.json"), }, { name: "test valid console", args: args{ - out: "console://", + out: []string{"console://"}, }, - want: &output.OutputConfig{ - Key: "console", + want: []output.OutputConfig{ + { + Key: "console", + }, }, err: nil, }, { name: "test valid json", args: args{ - out: "json:///tmp/foobar.json", + out: []string{"json:///tmp/foobar.json"}, }, - want: &output.OutputConfig{ - Key: "json", - Path: "/tmp/foobar.json", + want: []output.OutputConfig{ + { + Key: "json", + Path: "/tmp/foobar.json", + }, }, err: nil, }, { name: "test empty jsonplan", args: args{ - out: "plan://", + out: []string{"plan://"}, }, - want: nil, + want: []output.OutputConfig{}, err: fmt.Errorf("Invalid plan output 'plan://': \nMust be of kind: plan://PATH/TO/FILE.json"), }, { name: "test valid jsonplan", args: args{ - out: "plan:///tmp/foobar.json", + out: []string{"plan:///tmp/foobar.json"}, }, - want: &output.OutputConfig{ - Key: "plan", - Path: "/tmp/foobar.json", + want: []output.OutputConfig{ + { + Key: "plan", + Path: "/tmp/foobar.json", + }, }, err: nil, }, + { + name: "test multiple output values", + args: args{ + out: []string{"console:///dev/stdout", "json://result.json"}, + }, + want: []output.OutputConfig{ + { + Key: "console", + }, + { + Key: "json", + Path: "result.json", + }, + }, + err: nil, + }, + { + name: "test multiple output values with invalid value", + args: args{ + out: []string{"console:///dev/stdout", "invalid://result.json"}, + }, + want: []output.OutputConfig{ + { + Key: "console", + }, + }, + err: fmt.Errorf("Unsupported output 'invalid': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parseOutputFlag(tt.args.out) + got, err := parseOutputFlags(tt.args.out) if err != nil && err.Error() != tt.err.Error() { t.Fatalf("got error = '%v', expected '%v'", err, tt.err) } diff --git a/pkg/driftctl.go b/pkg/driftctl.go index 7d871ff2..d691ebbc 100644 --- a/pkg/driftctl.go +++ b/pkg/driftctl.go @@ -24,7 +24,7 @@ type ScanOptions struct { Detect bool From []config.SupplierConfig To string - Output output.OutputConfig + Output []output.OutputConfig Filter *jmespath.JMESPath Quiet bool BackendOptions *backend.Options