Merge pull request #865 from cloudskiff/feat/multipleOutput
Allow multiple output flags for a single scanmain
commit
1337db52ea
|
@ -61,12 +61,13 @@ func NewScanCmd() *cobra.Command {
|
|||
)
|
||||
}
|
||||
|
||||
outputFlag, _ := cmd.Flags().GetString("output")
|
||||
out, err := parseOutputFlag(outputFlag)
|
||||
outputFlag, _ := cmd.Flags().GetStringSlice("output")
|
||||
|
||||
out, err := parseOutputFlags(outputFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Output = *out
|
||||
opts.Output = out
|
||||
|
||||
filterFlag, _ := cmd.Flags().GetStringArray("filter")
|
||||
|
||||
|
@ -117,10 +118,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.Example(output.ConsoleOutputType)},
|
||||
"Output format, by default it will write to the console\n"+
|
||||
"Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n",
|
||||
)
|
||||
|
@ -190,7 +191,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 +256,21 @@ func scanRun(opts *pkg.ScanOptions) error {
|
|||
analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String()
|
||||
analysis.ProviderName = resourceSchemaRepository.ProviderName
|
||||
|
||||
err = selectedOutput.Write(analysis)
|
||||
if err != nil {
|
||||
return err
|
||||
validOutput := false
|
||||
for _, o := range opts.Output {
|
||||
if err = output.GetOutput(o, opts.Quiet).Write(analysis); err != nil {
|
||||
logrus.Errorf("Error writing to output %s: %v", o.String(), err.Error())
|
||||
continue
|
||||
}
|
||||
validOutput = true
|
||||
}
|
||||
|
||||
// Fallback to console output if all output failed
|
||||
if !validOutput {
|
||||
logrus.Debug("All outputs failed, fallback to console output")
|
||||
if err = output.NewConsole().Write(analysis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second)))
|
||||
|
@ -352,6 +364,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] == "" {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package output
|
||||
|
||||
import "fmt"
|
||||
|
||||
type OutputConfig struct {
|
||||
Key string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *OutputConfig) String() string {
|
||||
return fmt.Sprintf("%s://%s", o.Key, o.Path)
|
||||
}
|
||||
|
|
|
@ -25,10 +25,6 @@ var supportedOutputExample = map[string]string{
|
|||
PlanOutputType: PlanOutputExample,
|
||||
}
|
||||
|
||||
func SupportedOutputs() []string {
|
||||
return supportedOutputTypes
|
||||
}
|
||||
|
||||
func SupportedOutputsExample() []string {
|
||||
examples := make([]string, 0, len(supportedOutputExample))
|
||||
for _, ex := range supportedOutputExample {
|
||||
|
|
|
@ -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,161 @@ 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"),
|
||||
},
|
||||
{
|
||||
name: "test multiple valid output values",
|
||||
args: args{
|
||||
out: []string{"json://result1.json", "json://result2.json", "json://result3.json"},
|
||||
},
|
||||
want: []output.OutputConfig{
|
||||
{
|
||||
Key: "json",
|
||||
Path: "result1.json",
|
||||
},
|
||||
{
|
||||
Key: "json",
|
||||
Path: "result2.json",
|
||||
},
|
||||
{
|
||||
Key: "json",
|
||||
Path: "result3.json",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue