diff --git a/pkg/analyser/analysis.go b/pkg/analyser/analysis.go index 0bdaf8bf..88136f71 100644 --- a/pkg/analyser/analysis.go +++ b/pkg/analyser/analysis.go @@ -69,6 +69,7 @@ type GenDriftIgnoreOptions struct { ExcludeDeleted bool ExcludeDrifted bool InputPath string + OutputPath string } func (a Analysis) MarshalJSON() ([]byte, error) { diff --git a/pkg/cmd/gen_driftignore.go b/pkg/cmd/gen_driftignore.go index 995b3333..032284b7 100644 --- a/pkg/cmd/gen_driftignore.go +++ b/pkg/cmd/gen_driftignore.go @@ -3,7 +3,9 @@ package cmd import ( "encoding/json" "fmt" + "io" "os" + "time" "github.com/cloudskiff/driftctl/pkg/analyser" "github.com/pkg/errors" @@ -16,19 +18,25 @@ func NewGenDriftIgnoreCmd() *cobra.Command { cmd := &cobra.Command{ Use: "gen-driftignore", Short: "Generate a .driftignore file based on your scan result", - Long: "This command will generate a new .driftignore file containing your current drifts and send output to /dev/stdout\n\nExample: driftctl scan -o json://stdout | driftctl gen-driftignore -i /dev/stdin > .driftignore", + Long: "This command will generate a new .driftignore file containing your current drifts\n\nExample: driftctl scan -o json://stdout | driftctl gen-driftignore", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - if opts.InputPath == "" { - return errors.New("Error: you must specify an input to parse JSON from. Use driftctl gen-driftignore -i \nGenerate a JSON file using the output flag: driftctl scan -o json://path/to/drifts.json") - } - _, list, err := genDriftIgnore(opts) if err != nil { return err } - fmt.Println(list) + ignoreFile := os.Stdout + if opts.OutputPath != "-" { + var err error + ignoreFile, err = os.OpenFile(opts.OutputPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return errors.Errorf("error opening output file: %s", err) + } + defer ignoreFile.Close() + fmt.Fprintf(os.Stderr, "Appending ignore rules to %s\n", opts.OutputPath) + } + fmt.Fprintf(ignoreFile, "# Generated by gen-driftignore cmd @ %s\n%s\n", time.Now().Format(time.RFC1123), list) return nil }, @@ -39,13 +47,24 @@ func NewGenDriftIgnoreCmd() *cobra.Command { fl.BoolVar(&opts.ExcludeUnmanaged, "exclude-unmanaged", false, "Exclude resources not managed by IaC") fl.BoolVar(&opts.ExcludeDeleted, "exclude-missing", false, "Exclude missing resources") fl.BoolVar(&opts.ExcludeDrifted, "exclude-changed", false, "Exclude resources that changed on cloud provider") - fl.StringVarP(&opts.InputPath, "input", "i", "", "Input where the JSON should be parsed from") + fl.StringVarP(&opts.InputPath, "input", "i", "-", "Input where the JSON should be parsed from. Defaults to stdin.") + fl.StringVarP(&opts.OutputPath, "output", "o", ".driftignore", "Output file path to write the driftignore to.") return cmd } func genDriftIgnore(opts *analyser.GenDriftIgnoreOptions) (int, string, error) { - input, err := os.ReadFile(opts.InputPath) + driftFile := os.Stdin + if opts.InputPath != "-" { + var err error + driftFile, err = os.Open(opts.InputPath) + if err != nil { + return 0, "", err + } + defer driftFile.Close() + } + + input, err := io.ReadAll(driftFile) if err != nil { return 0, "", err } diff --git a/pkg/cmd/gen_driftignore_test.go b/pkg/cmd/gen_driftignore_test.go index e7816374..3fc4bc05 100644 --- a/pkg/cmd/gen_driftignore_test.go +++ b/pkg/cmd/gen_driftignore_test.go @@ -1,15 +1,15 @@ package cmd import ( - "bytes" "errors" - "io" "os" + "strings" "testing" "github.com/cloudskiff/driftctl/test" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGenDriftIgnoreCmd_Input(t *testing.T) { @@ -55,12 +55,6 @@ func TestGenDriftIgnoreCmd_Input(t *testing.T) { output: "./testdata/output_stdin_valid_filter2.txt", err: errors.New("open doesnotexist: no such file or directory"), }, - { - name: "test error when input flag is not specified", - args: []string{}, - output: "", - err: errors.New("Error: you must specify an input to parse JSON from. Use driftctl gen-driftignore -i \nGenerate a JSON file using the output flag: driftctl scan -o json://path/to/drifts.json"), - }, } for _, c := range cases { @@ -68,13 +62,16 @@ func TestGenDriftIgnoreCmd_Input(t *testing.T) { rootCmd := &cobra.Command{Use: "root"} rootCmd.AddCommand(NewGenDriftIgnoreCmd()) - stdout := os.Stdout // keep backup of the real stdout - r, w, _ := os.Pipe() - os.Stdout = w + f, err := os.CreateTemp("", "TestGenDriftIgnoreCmd_Input") + require.Nil(t, err) + defer func() { + f.Close() + os.Remove(f.Name()) + }() - args := append([]string{"gen-driftignore"}, c.args...) + args := append([]string{"gen-driftignore", "-o", f.Name()}, c.args...) - _, err := test.Execute(rootCmd, args...) + _, err = test.Execute(rootCmd, args...) if c.err != nil { assert.EqualError(t, err, c.err.Error()) return @@ -82,26 +79,13 @@ func TestGenDriftIgnoreCmd_Input(t *testing.T) { assert.Equal(t, c.err, err) } - outC := make(chan []byte) - // copy the output in a separate goroutine so printing can't block indefinitely - go func() { - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) - outC <- buf.Bytes() - }() - - // back to normal state - w.Close() - os.Stdout = stdout // restoring the real stdout - result := <-outC + output, err := os.ReadFile(f.Name()) + require.Nil(t, err) if c.output != "" { - output, err := os.ReadFile(c.output) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, string(output), string(result)) + expectedOutput, err := os.ReadFile(c.output) + require.Nil(t, err) + assert.Equal(t, string(expectedOutput), trimLeadingComment(string(output))) } }) } @@ -116,11 +100,12 @@ func TestGenDriftIgnoreCmd_ValidFlags(t *testing.T) { cases := []struct { args []string }{ + {args: []string{"gen-driftignore"}}, {args: []string{"gen-driftignore", "--exclude-unmanaged"}}, {args: []string{"gen-driftignore", "--exclude-missing"}}, {args: []string{"gen-driftignore", "--exclude-changed"}}, {args: []string{"gen-driftignore", "--exclude-changed=false", "--exclude-missing=false", "--exclude-unmanaged=true"}}, - {args: []string{"gen-driftignore", "--input", "/dev/stdin"}}, + {args: []string{"gen-driftignore", "--input", "-"}}, {args: []string{"gen-driftignore", "-i", "/dev/stdout"}}, } @@ -158,3 +143,10 @@ func TestGenDriftIgnoreCmd_InvalidFlags(t *testing.T) { assert.EqualError(t, err, tt.err.Error()) } } + +// The leading comment, "Generated by gen-driftignore..." contains a timestamp, +// that we don't care to assert on. +func trimLeadingComment(content string) string { + lines := strings.Split(content, "\n") + return strings.Join(lines[1:], "\n") +}