feat: allow multiple output flags

main
sundowndev 2021-07-21 17:57:05 +02:00
parent 6586aa1311
commit 8036b7a702
3 changed files with 98 additions and 37 deletions

View File

@ -61,12 +61,16 @@ func NewScanCmd() *cobra.Command {
) )
} }
outputFlag, _ := cmd.Flags().GetString("output") outputFlag, _ := cmd.Flags().GetStringSlice("output")
out, err := parseOutputFlag(outputFlag) if len(outputFlag) == 0 {
outputFlag = append(outputFlag, output.Example(output.ConsoleOutputType))
}
out, err := parseOutputFlags(outputFlag)
if err != nil { if err != nil {
return err return err
} }
opts.Output = *out opts.Output = out
filterFlag, _ := cmd.Flags().GetStringArray("filter") 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"+ " - 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", " - Attr.Tags.Terraform == 'true' (include only resources that have Tag Terraform equal to 'true')\n",
) )
fl.StringP( fl.StringSliceP(
"output", "output",
"o", "o",
output.Example(output.ConsoleOutputType), []string{},
"Output format, by default it will write to the console\n"+ "Output format, by default it will write to the console\n"+
"Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n", "Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n",
) )
@ -190,7 +194,6 @@ func NewScanCmd() *cobra.Command {
func scanRun(opts *pkg.ScanOptions) error { func scanRun(opts *pkg.ScanOptions) error {
store := memstore.New() store := memstore.New()
selectedOutput := output.GetOutput(opts.Output, opts.Quiet)
c := make(chan os.Signal) c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@ -256,10 +259,13 @@ func scanRun(opts *pkg.ScanOptions) error {
analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String() analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String()
analysis.ProviderName = resourceSchemaRepository.ProviderName analysis.ProviderName = resourceSchemaRepository.ProviderName
for _, o := range opts.Output {
selectedOutput := output.GetOutput(o, opts.Quiet)
err = selectedOutput.Write(analysis) err = selectedOutput.Write(analysis)
if err != nil { if err != nil {
return err return err
} }
}
globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second))) globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second)))
globaloutput.Printf(color.WhiteString("Provider version used to scan: %s. Use --tf-provider-version to use another version.\n"), resourceSchemaRepository.ProviderVersion.String()) globaloutput.Printf(color.WhiteString("Provider version used to scan: %s. Use --tf-provider-version to use another version.\n"), resourceSchemaRepository.ProviderVersion.String())
@ -352,6 +358,18 @@ func parseFromFlag(from []string) ([]config.SupplierConfig, error) {
return configs, nil 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) { func parseOutputFlag(out string) (*output.OutputConfig, error) {
schemeOpts := strings.Split(out, "://") schemeOpts := strings.Split(out, "://")
if len(schemeOpts) < 2 || schemeOpts[0] == "" { if len(schemeOpts) < 2 || schemeOpts[0] == "" {

View File

@ -48,6 +48,7 @@ func TestScanCmd_Valid(t *testing.T) {
{args: []string{"scan", "--tf-provider-version", "3.30.2"}}, {args: []string{"scan", "--tf-provider-version", "3.30.2"}},
{args: []string{"scan", "--driftignore", "./path/to/driftignore.s3"}}, {args: []string{"scan", "--driftignore", "./path/to/driftignore.s3"}},
{args: []string{"scan", "--driftignore", ".driftignore"}}, {args: []string{"scan", "--driftignore", ".driftignore"}},
{args: []string{"scan", "-o", "html://result.html", "-o", "json://result.json"}},
} }
for _, tt := range cases { for _, tt := range cases {
@ -164,98 +165,140 @@ func Test_parseFromFlag(t *testing.T) {
func Test_parseOutputFlag(t *testing.T) { func Test_parseOutputFlag(t *testing.T) {
type args struct { type args struct {
out string out []string
} }
tests := []struct { tests := []struct {
name string name string
args args args args
want *output.OutputConfig want []output.OutputConfig
err error err error
}{ }{
{ {
name: "test empty", name: "test empty output",
args: args{ 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"), 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", name: "test invalid",
args: args{ 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"), 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", name: "test invalid",
args: args{ 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"), 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", name: "test unsupported",
args: args{ 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"), 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", name: "test empty json",
args: args{ 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"), err: fmt.Errorf("Invalid json output 'json://': \nMust be of kind: json://PATH/TO/FILE.json"),
}, },
{ {
name: "test valid console", name: "test valid console",
args: args{ args: args{
out: "console://", out: []string{"console://"},
}, },
want: &output.OutputConfig{ want: []output.OutputConfig{
{
Key: "console", Key: "console",
}, },
},
err: nil, err: nil,
}, },
{ {
name: "test valid json", name: "test valid json",
args: args{ args: args{
out: "json:///tmp/foobar.json", out: []string{"json:///tmp/foobar.json"},
}, },
want: &output.OutputConfig{ want: []output.OutputConfig{
{
Key: "json", Key: "json",
Path: "/tmp/foobar.json", Path: "/tmp/foobar.json",
}, },
},
err: nil, err: nil,
}, },
{ {
name: "test empty jsonplan", name: "test empty jsonplan",
args: args{ 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"), err: fmt.Errorf("Invalid plan output 'plan://': \nMust be of kind: plan://PATH/TO/FILE.json"),
}, },
{ {
name: "test valid jsonplan", name: "test valid jsonplan",
args: args{ args: args{
out: "plan:///tmp/foobar.json", out: []string{"plan:///tmp/foobar.json"},
}, },
want: &output.OutputConfig{ want: []output.OutputConfig{
{
Key: "plan", Key: "plan",
Path: "/tmp/foobar.json", Path: "/tmp/foobar.json",
}, },
},
err: nil, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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() { if err != nil && err.Error() != tt.err.Error() {
t.Fatalf("got error = '%v', expected '%v'", err, tt.err) t.Fatalf("got error = '%v', expected '%v'", err, tt.err)
} }

View File

@ -24,7 +24,7 @@ type ScanOptions struct {
Detect bool Detect bool
From []config.SupplierConfig From []config.SupplierConfig
To string To string
Output output.OutputConfig Output []output.OutputConfig
Filter *jmespath.JMESPath Filter *jmespath.JMESPath
Quiet bool Quiet bool
BackendOptions *backend.Options BackendOptions *backend.Options