Skip to content

Commit 1337db5

Browse files
authored
Merge pull request #865 from cloudskiff/feat/multipleOutput
Allow multiple output flags for a single scan
2 parents 6586aa1 + 6e48f17 commit 1337db5

5 files changed

Lines changed: 131 additions & 41 deletions

File tree

pkg/cmd/scan.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,13 @@ func NewScanCmd() *cobra.Command {
6161
)
6262
}
6363

64-
outputFlag, _ := cmd.Flags().GetString("output")
65-
out, err := parseOutputFlag(outputFlag)
64+
outputFlag, _ := cmd.Flags().GetStringSlice("output")
65+
66+
out, err := parseOutputFlags(outputFlag)
6667
if err != nil {
6768
return err
6869
}
69-
opts.Output = *out
70+
opts.Output = out
7071

7172
filterFlag, _ := cmd.Flags().GetStringArray("filter")
7273

@@ -117,10 +118,10 @@ func NewScanCmd() *cobra.Command {
117118
" - Type =='aws_s3_bucket && Id != 'my_bucket' (excludes s3 bucket 'my_bucket')\n"+
118119
" - Attr.Tags.Terraform == 'true' (include only resources that have Tag Terraform equal to 'true')\n",
119120
)
120-
fl.StringP(
121+
fl.StringSliceP(
121122
"output",
122123
"o",
123-
output.Example(output.ConsoleOutputType),
124+
[]string{output.Example(output.ConsoleOutputType)},
124125
"Output format, by default it will write to the console\n"+
125126
"Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n",
126127
)
@@ -190,7 +191,6 @@ func NewScanCmd() *cobra.Command {
190191

191192
func scanRun(opts *pkg.ScanOptions) error {
192193
store := memstore.New()
193-
selectedOutput := output.GetOutput(opts.Output, opts.Quiet)
194194

195195
c := make(chan os.Signal)
196196
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@@ -256,9 +256,21 @@ func scanRun(opts *pkg.ScanOptions) error {
256256
analysis.ProviderVersion = resourceSchemaRepository.ProviderVersion.String()
257257
analysis.ProviderName = resourceSchemaRepository.ProviderName
258258

259-
err = selectedOutput.Write(analysis)
260-
if err != nil {
261-
return err
259+
validOutput := false
260+
for _, o := range opts.Output {
261+
if err = output.GetOutput(o, opts.Quiet).Write(analysis); err != nil {
262+
logrus.Errorf("Error writing to output %s: %v", o.String(), err.Error())
263+
continue
264+
}
265+
validOutput = true
266+
}
267+
268+
// Fallback to console output if all output failed
269+
if !validOutput {
270+
logrus.Debug("All outputs failed, fallback to console output")
271+
if err = output.NewConsole().Write(analysis); err != nil {
272+
return err
273+
}
262274
}
263275

264276
globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second)))
@@ -352,6 +364,18 @@ func parseFromFlag(from []string) ([]config.SupplierConfig, error) {
352364
return configs, nil
353365
}
354366

367+
func parseOutputFlags(out []string) ([]output.OutputConfig, error) {
368+
result := make([]output.OutputConfig, 0, len(out))
369+
for _, v := range out {
370+
o, err := parseOutputFlag(v)
371+
if err != nil {
372+
return result, err
373+
}
374+
result = append(result, *o)
375+
}
376+
return result, nil
377+
}
378+
355379
func parseOutputFlag(out string) (*output.OutputConfig, error) {
356380
schemeOpts := strings.Split(out, "://")
357381
if len(schemeOpts) < 2 || schemeOpts[0] == "" {

pkg/cmd/scan/output/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package output
22

3+
import "fmt"
4+
35
type OutputConfig struct {
46
Key string
57
Path string
68
}
9+
10+
func (o *OutputConfig) String() string {
11+
return fmt.Sprintf("%s://%s", o.Key, o.Path)
12+
}

pkg/cmd/scan/output/output.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ var supportedOutputExample = map[string]string{
2525
PlanOutputType: PlanOutputExample,
2626
}
2727

28-
func SupportedOutputs() []string {
29-
return supportedOutputTypes
30-
}
31-
3228
func SupportedOutputsExample() []string {
3329
examples := make([]string, 0, len(supportedOutputExample))
3430
for _, ex := range supportedOutputExample {

pkg/cmd/scan_test.go

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func TestScanCmd_Valid(t *testing.T) {
4848
{args: []string{"scan", "--tf-provider-version", "3.30.2"}},
4949
{args: []string{"scan", "--driftignore", "./path/to/driftignore.s3"}},
5050
{args: []string{"scan", "--driftignore", ".driftignore"}},
51+
{args: []string{"scan", "-o", "html://result.html", "-o", "json://result.json"}},
5152
}
5253

5354
for _, tt := range cases {
@@ -164,98 +165,161 @@ func Test_parseFromFlag(t *testing.T) {
164165

165166
func Test_parseOutputFlag(t *testing.T) {
166167
type args struct {
167-
out string
168+
out []string
168169
}
169170
tests := []struct {
170171
name string
171172
args args
172-
want *output.OutputConfig
173+
want []output.OutputConfig
173174
err error
174175
}{
175176
{
176-
name: "test empty",
177+
name: "test empty output",
177178
args: args{
178-
out: "",
179+
out: []string{""},
179180
},
180-
want: nil,
181+
want: []output.OutputConfig{},
181182
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"),
182183
},
184+
{
185+
name: "test empty array",
186+
args: args{
187+
out: []string{},
188+
},
189+
want: []output.OutputConfig{},
190+
err: nil,
191+
},
183192
{
184193
name: "test invalid",
185194
args: args{
186-
out: "sdgjsdgjsdg",
195+
out: []string{"sdgjsdgjsdg"},
187196
},
188-
want: nil,
197+
want: []output.OutputConfig{},
189198
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"),
190199
},
191200
{
192201
name: "test invalid",
193202
args: args{
194-
out: "://",
203+
out: []string{"://"},
195204
},
196-
want: nil,
205+
want: []output.OutputConfig{},
197206
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"),
198207
},
199208
{
200209
name: "test unsupported",
201210
args: args{
202-
out: "foobar://",
211+
out: []string{"foobar://"},
203212
},
204-
want: nil,
213+
want: []output.OutputConfig{},
205214
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"),
206215
},
207216
{
208217
name: "test empty json",
209218
args: args{
210-
out: "json://",
219+
out: []string{"json://"},
211220
},
212-
want: nil,
221+
want: []output.OutputConfig{},
213222
err: fmt.Errorf("Invalid json output 'json://': \nMust be of kind: json://PATH/TO/FILE.json"),
214223
},
215224
{
216225
name: "test valid console",
217226
args: args{
218-
out: "console://",
227+
out: []string{"console://"},
219228
},
220-
want: &output.OutputConfig{
221-
Key: "console",
229+
want: []output.OutputConfig{
230+
{
231+
Key: "console",
232+
},
222233
},
223234
err: nil,
224235
},
225236
{
226237
name: "test valid json",
227238
args: args{
228-
out: "json:///tmp/foobar.json",
239+
out: []string{"json:///tmp/foobar.json"},
229240
},
230-
want: &output.OutputConfig{
231-
Key: "json",
232-
Path: "/tmp/foobar.json",
241+
want: []output.OutputConfig{
242+
{
243+
Key: "json",
244+
Path: "/tmp/foobar.json",
245+
},
233246
},
234247
err: nil,
235248
},
236249
{
237250
name: "test empty jsonplan",
238251
args: args{
239-
out: "plan://",
252+
out: []string{"plan://"},
240253
},
241-
want: nil,
254+
want: []output.OutputConfig{},
242255
err: fmt.Errorf("Invalid plan output 'plan://': \nMust be of kind: plan://PATH/TO/FILE.json"),
243256
},
244257
{
245258
name: "test valid jsonplan",
246259
args: args{
247-
out: "plan:///tmp/foobar.json",
260+
out: []string{"plan:///tmp/foobar.json"},
261+
},
262+
want: []output.OutputConfig{
263+
{
264+
Key: "plan",
265+
Path: "/tmp/foobar.json",
266+
},
267+
},
268+
err: nil,
269+
},
270+
{
271+
name: "test multiple output values",
272+
args: args{
273+
out: []string{"console:///dev/stdout", "json://result.json"},
248274
},
249-
want: &output.OutputConfig{
250-
Key: "plan",
251-
Path: "/tmp/foobar.json",
275+
want: []output.OutputConfig{
276+
{
277+
Key: "console",
278+
},
279+
{
280+
Key: "json",
281+
Path: "result.json",
282+
},
283+
},
284+
err: nil,
285+
},
286+
{
287+
name: "test multiple output values with invalid value",
288+
args: args{
289+
out: []string{"console:///dev/stdout", "invalid://result.json"},
290+
},
291+
want: []output.OutputConfig{
292+
{
293+
Key: "console",
294+
},
295+
},
296+
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"),
297+
},
298+
{
299+
name: "test multiple valid output values",
300+
args: args{
301+
out: []string{"json://result1.json", "json://result2.json", "json://result3.json"},
302+
},
303+
want: []output.OutputConfig{
304+
{
305+
Key: "json",
306+
Path: "result1.json",
307+
},
308+
{
309+
Key: "json",
310+
Path: "result2.json",
311+
},
312+
{
313+
Key: "json",
314+
Path: "result3.json",
315+
},
252316
},
253317
err: nil,
254318
},
255319
}
256320
for _, tt := range tests {
257321
t.Run(tt.name, func(t *testing.T) {
258-
got, err := parseOutputFlag(tt.args.out)
322+
got, err := parseOutputFlags(tt.args.out)
259323
if err != nil && err.Error() != tt.err.Error() {
260324
t.Fatalf("got error = '%v', expected '%v'", err, tt.err)
261325
}

pkg/driftctl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type ScanOptions struct {
2424
Detect bool
2525
From []config.SupplierConfig
2626
To string
27-
Output output.OutputConfig
27+
Output []output.OutputConfig
2828
Filter *jmespath.JMESPath
2929
Quiet bool
3030
BackendOptions *backend.Options

0 commit comments

Comments
 (0)