feat: allow multiple output flags
parent
6586aa1311
commit
8036b7a702
|
@ -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,9 +259,12 @@ func scanRun(opts *pkg.ScanOptions) error {
|
||||||
analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String()
|
analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String()
|
||||||
analysis.ProviderName = resourceSchemaRepository.ProviderName
|
analysis.ProviderName = resourceSchemaRepository.ProviderName
|
||||||
|
|
||||||
err = selectedOutput.Write(analysis)
|
for _, o := range opts.Output {
|
||||||
if err != nil {
|
selectedOutput := output.GetOutput(o, opts.Quiet)
|
||||||
return err
|
err = selectedOutput.Write(analysis)
|
||||||
|
if err != nil {
|
||||||
|
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)))
|
||||||
|
@ -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] == "" {
|
||||||
|
|
|
@ -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",
|
{
|
||||||
Path: "/tmp/foobar.json",
|
Key: "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",
|
{
|
||||||
Path: "/tmp/foobar.json",
|
Key: "plan",
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue