diff --git a/Makefile b/Makefile index 928db061a..5d4d6b203 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ deps: .PHONY: install-tools install-tools: $(GOGET) gotest.tools/gotestsum - $(GOGET) github.com/vektra/mockery/.../ + $(GOGET) github.com/vektra/mockery/v2/.../ go.mod: FORCE diff --git a/pkg/cmd/scan.go b/pkg/cmd/scan.go index a1e3bf4da..83b253ddd 100644 --- a/pkg/cmd/scan.go +++ b/pkg/cmd/scan.go @@ -20,6 +20,7 @@ import ( "github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/iac/supplier" "github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend" + globaloutput "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/remote" "github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/terraform" @@ -32,6 +33,7 @@ type ScanOptions struct { To string Output output.OutputConfig Filter *jmespath.JMESPath + Quiet bool } func NewScanCmd() *cobra.Command { @@ -77,6 +79,8 @@ func NewScanCmd() *cobra.Command { opts.Filter = expr } + opts.Quiet, _ = cmd.Flags().GetBool("quiet") + return nil }, RunE: func(cmd *cobra.Command, args []string) error { @@ -85,6 +89,12 @@ func NewScanCmd() *cobra.Command { } fl := cmd.Flags() + fl.BoolP( + "quiet", + "", + false, + "Do not display anything but scan results", + ) fl.StringP( "filter", "", @@ -123,7 +133,7 @@ func NewScanCmd() *cobra.Command { } func scanRun(opts *ScanOptions) error { - selectedOutput := output.GetOutput(opts.Output) + selectedOutput := output.GetOutput(opts.Output, opts.Quiet) c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) @@ -132,7 +142,9 @@ func scanRun(opts *ScanOptions) error { providerLibrary := terraform.NewProviderLibrary() supplierLibrary := resource.NewSupplierLibrary() - err := remote.Activate(opts.To, alerter, providerLibrary, supplierLibrary) + progress := globaloutput.NewProgress() + + err := remote.Activate(opts.To, alerter, providerLibrary, supplierLibrary, progress) if err != nil { return err } @@ -158,7 +170,9 @@ func scanRun(opts *ScanOptions) error { ctl.Stop() }() + progress.Start() analysis, err := ctl.Run() + progress.Stop() if err != nil { return err diff --git a/pkg/cmd/scan/output/output.go b/pkg/cmd/scan/output/output.go index cb10170f2..1d960f8fe 100644 --- a/pkg/cmd/scan/output/output.go +++ b/pkg/cmd/scan/output/output.go @@ -47,8 +47,8 @@ func IsSupported(key string) bool { return false } -func GetOutput(config OutputConfig) Output { - output.ChangePrinter(GetPrinter(config)) +func GetOutput(config OutputConfig, quiet bool) Output { + output.ChangePrinter(GetPrinter(config, quiet)) switch config.Key { case JSONOutputType: @@ -60,7 +60,11 @@ func GetOutput(config OutputConfig) Output { } } -func GetPrinter(config OutputConfig) output.Printer { +func GetPrinter(config OutputConfig, quiet bool) output.Printer { + if quiet { + return &output.VoidPrinter{} + } + switch config.Key { case JSONOutputType: if isStdOut(config.Options["path"]) { diff --git a/pkg/cmd/scan/output/output_test.go b/pkg/cmd/scan/output/output_test.go index 3e227ffcb..f3e11f12f 100644 --- a/pkg/cmd/scan/output/output_test.go +++ b/pkg/cmd/scan/output/output_test.go @@ -267,10 +267,11 @@ func fakeAnalysisWithGithubEnumerationError() *analyser.Analysis { func TestGetPrinter(t *testing.T) { tests := []struct { - name string - path string - key string - want output.Printer + name string + path string + key string + quiet bool + want output.Printer }{ { name: "json file output", @@ -278,6 +279,13 @@ func TestGetPrinter(t *testing.T) { key: JSONOutputType, want: output.NewConsolePrinter(), }, + { + name: "json file output quiet", + path: "/path/to/file", + key: JSONOutputType, + quiet: true, + want: &output.VoidPrinter{}, + }, { name: "json stdout output", path: "stdout", @@ -296,6 +304,13 @@ func TestGetPrinter(t *testing.T) { key: ConsoleOutputType, want: output.NewConsolePrinter(), }, + { + name: "quiet console stdout output", + path: "stdout", + quiet: true, + key: ConsoleOutputType, + want: &output.VoidPrinter{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -304,7 +319,7 @@ func TestGetPrinter(t *testing.T) { Options: map[string]string{ "path": tt.path, }, - }); !reflect.DeepEqual(got, tt.want) { + }, tt.quiet); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetPrinter() = %v, want %v", got, tt.want) } }) diff --git a/pkg/iac/terraform/state/terraform_state_reader_test.go b/pkg/iac/terraform/state/terraform_state_reader_test.go index 48f8f80b4..126b76daf 100644 --- a/pkg/iac/terraform/state/terraform_state_reader_test.go +++ b/pkg/iac/terraform/state/terraform_state_reader_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/iac" "github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/remote/aws" @@ -96,7 +98,9 @@ func TestTerraformStateReader_AWS_Resources(t *testing.T) { if shouldUpdate { var err error - realProvider, err = aws.NewAWSTerraformProvider() + progress := &output.MockProgress{} + progress.On("Inc").Return() + realProvider, err = aws.NewAWSTerraformProvider(progress) if err != nil { t.Fatal(err) } @@ -171,7 +175,9 @@ func TestTerraformStateReader_Github_Resources(t *testing.T) { if shouldUpdate { var err error - realProvider, err = github.NewGithubTerraformProvider() + progress := &output.MockProgress{} + progress.On("Inc").Return() + realProvider, err = github.NewGithubTerraformProvider(progress) if err != nil { t.Fatal(err) } diff --git a/pkg/output/mock_Progress.go b/pkg/output/mock_Progress.go new file mode 100644 index 000000000..46619dc39 --- /dev/null +++ b/pkg/output/mock_Progress.go @@ -0,0 +1,39 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package output + +import mock "github.com/stretchr/testify/mock" + +// MockProgress is an autogenerated mock type for the Progress type +type MockProgress struct { + mock.Mock +} + +// Inc provides a mock function with given fields: +func (_m *MockProgress) Inc() { + _m.Called() +} + +// Start provides a mock function with given fields: +func (_m *MockProgress) Start() { + _m.Called() +} + +// Stop provides a mock function with given fields: +func (_m *MockProgress) Stop() { + _m.Called() +} + +// Val provides a mock function with given fields: +func (_m *MockProgress) Val() uint64 { + ret := _m.Called() + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} diff --git a/pkg/output/progress.go b/pkg/output/progress.go new file mode 100644 index 000000000..97ed60f9d --- /dev/null +++ b/pkg/output/progress.go @@ -0,0 +1,98 @@ +package output + +import ( + "time" + + "go.uber.org/atomic" + + "github.com/sirupsen/logrus" +) + +var spinner = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"} + +const ( + progressTimeout = 10 * time.Second + progressRefreshRate = 200 * time.Millisecond +) + +type Progress interface { + Start() + Stop() + Inc() + Val() uint64 +} + +type progress struct { + ticChan chan struct{} + endChan chan struct{} + started *atomic.Bool + count *atomic.Uint64 +} + +func NewProgress() *progress { + return &progress{ + make(chan struct{}), + make(chan struct{}), + atomic.NewBool(false), + atomic.NewUint64(0), + } +} + +func (p *progress) Start() { + if !p.started.Swap(true) { + go p.watch() + go p.render() + } +} + +func (p *progress) Stop() { + if p.started.Swap(false) { + p.endChan <- struct{}{} + Printf("\n") + } +} + +func (p *progress) Inc() { + if p.started.Load() { + p.ticChan <- struct{}{} + } +} + +func (p *progress) Val() uint64 { + return p.count.Load() +} + +func (p *progress) render() { + i := -1 + Printf("Scanning resources:\r") + for { + select { + case <-p.endChan: + return + case <-time.After(progressRefreshRate): + i++ + if i >= len(spinner) { + i = 0 + } + Printf("Scanning resources: %s (%d)\r", spinner[i], p.count.Load()) + } + } +} + +func (p *progress) watch() { +Loop: + for { + select { + case <-p.ticChan: + p.count.Inc() + continue Loop + case <-time.After(progressTimeout): + p.started.Store(false) + break Loop + case <-p.endChan: + return + } + } + logrus.Debug("Progress did not receive any tic. Stopping...") + p.endChan <- struct{}{} +} diff --git a/pkg/output/progress_test.go b/pkg/output/progress_test.go new file mode 100644 index 000000000..4b6073f7d --- /dev/null +++ b/pkg/output/progress_test.go @@ -0,0 +1,27 @@ +package output + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestProgressTimeout(t *testing.T) { + progress := NewProgress() + progress.Start() + time.Sleep(progressTimeout + 1) + progress.Inc() // should not hang + progress.Stop() // should not hang + assert.Equal(t, uint64(0), progress.Val()) +} + +func TestProgress(t *testing.T) { + progress := NewProgress() + progress.Start() + progress.Inc() + progress.Inc() + progress.Inc() + progress.Stop() + assert.Equal(t, uint64(3), progress.Val()) +} diff --git a/pkg/remote/aws/init.go b/pkg/remote/aws/init.go index 9bd719e32..21102c211 100644 --- a/pkg/remote/aws/init.go +++ b/pkg/remote/aws/init.go @@ -2,6 +2,7 @@ package aws import ( "github.com/cloudskiff/driftctl/pkg/alerter" + "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/remote/aws/client" "github.com/cloudskiff/driftctl/pkg/remote/aws/repository" "github.com/cloudskiff/driftctl/pkg/resource" @@ -14,8 +15,8 @@ const RemoteAWSTerraform = "aws+tf" * Initialize remote (configure credentials, launch tf providers and start gRPC clients) * Required to use Scanner */ -func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary) error { - provider, err := NewAWSTerraformProvider() +func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { + provider, err := NewAWSTerraformProvider(progress) if err != nil { return err } diff --git a/pkg/remote/aws/init_test.go b/pkg/remote/aws/init_test.go index 8eb84cd54..30e6ccc90 100644 --- a/pkg/remote/aws/init_test.go +++ b/pkg/remote/aws/init_test.go @@ -1,9 +1,12 @@ package aws -import "github.com/cloudskiff/driftctl/pkg/terraform" +import ( + "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/terraform" +) func InitTestAwsProvider(providerLibrary *terraform.ProviderLibrary) (*AWSTerraformProvider, error) { - provider, err := NewAWSTerraformProvider() + provider, err := NewAWSTerraformProvider(&output.MockProgress{}) if err != nil { return nil, err } diff --git a/pkg/remote/aws/provider.go b/pkg/remote/aws/provider.go index 32a807afb..bbc67c42b 100644 --- a/pkg/remote/aws/provider.go +++ b/pkg/remote/aws/provider.go @@ -3,6 +3,7 @@ package aws import ( "github.com/aws/aws-sdk-go/aws/session" + "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/remote/terraform" tf "github.com/cloudskiff/driftctl/pkg/terraform" ) @@ -41,7 +42,7 @@ type AWSTerraformProvider struct { session *session.Session } -func NewAWSTerraformProvider() (*AWSTerraformProvider, error) { +func NewAWSTerraformProvider(progress output.Progress) (*AWSTerraformProvider, error) { p := &AWSTerraformProvider{} providerKey := "aws" installer, err := tf.NewProviderInstaller(tf.ProviderConfig{ @@ -64,7 +65,7 @@ func NewAWSTerraformProvider() (*AWSTerraformProvider, error) { MaxRetries: 10, // TODO make this configurable } }, - }) + }, progress) if err != nil { return nil, err } diff --git a/pkg/remote/github/init.go b/pkg/remote/github/init.go index 5b33be283..b8088cab5 100644 --- a/pkg/remote/github/init.go +++ b/pkg/remote/github/init.go @@ -2,6 +2,7 @@ package github import ( "github.com/cloudskiff/driftctl/pkg/alerter" + "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/terraform" ) @@ -12,8 +13,8 @@ const RemoteGithubTerraform = "github+tf" * Initialize remote (configure credentials, launch tf providers and start gRPC clients) * Required to use Scanner */ -func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary) error { - provider, err := NewGithubTerraformProvider() +func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { + provider, err := NewGithubTerraformProvider(progress) if err != nil { return err } diff --git a/pkg/remote/github/init_test.go b/pkg/remote/github/init_test.go index 476c2c2ee..916902eb4 100644 --- a/pkg/remote/github/init_test.go +++ b/pkg/remote/github/init_test.go @@ -1,9 +1,12 @@ package github -import "github.com/cloudskiff/driftctl/pkg/terraform" +import ( + "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/terraform" +) func InitTestGithubProvider(providerLibrary *terraform.ProviderLibrary) (*GithubTerraformProvider, error) { - provider, err := NewGithubTerraformProvider() + provider, err := NewGithubTerraformProvider(&output.MockProgress{}) if err != nil { return nil, err } diff --git a/pkg/remote/github/provider.go b/pkg/remote/github/provider.go index 9ced044e1..938bd124b 100644 --- a/pkg/remote/github/provider.go +++ b/pkg/remote/github/provider.go @@ -3,6 +3,8 @@ package github import ( "os" + "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/remote/terraform" tf "github.com/cloudskiff/driftctl/pkg/terraform" ) @@ -17,7 +19,7 @@ type githubConfig struct { Organization string } -func NewGithubTerraformProvider() (*GithubTerraformProvider, error) { +func NewGithubTerraformProvider(progress output.Progress) (*GithubTerraformProvider, error) { p := &GithubTerraformProvider{} providerKey := "github" installer, err := tf.NewProviderInstaller(tf.ProviderConfig{ @@ -35,7 +37,7 @@ func NewGithubTerraformProvider() (*GithubTerraformProvider, error) { Owner: p.GetConfig().getDefaultOwner(), } }, - }) + }, progress) if err != nil { return nil, err } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 09d422ea7..429a61e81 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -2,6 +2,7 @@ package remote import ( "github.com/cloudskiff/driftctl/pkg/alerter" + "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/remote/aws" "github.com/cloudskiff/driftctl/pkg/remote/github" "github.com/cloudskiff/driftctl/pkg/resource" @@ -23,12 +24,12 @@ func IsSupported(remote string) bool { return false } -func Activate(remote string, alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary) error { +func Activate(remote string, alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { switch remote { case aws.RemoteAWSTerraform: - return aws.Init(alerter, providerLibrary, supplierLibrary) + return aws.Init(alerter, providerLibrary, supplierLibrary, progress) case github.RemoteGithubTerraform: - return github.Init(alerter, providerLibrary, supplierLibrary) + return github.Init(alerter, providerLibrary, supplierLibrary, progress) default: return errors.Errorf("unsupported remote '%s'", remote) } diff --git a/pkg/remote/terraform/provider.go b/pkg/remote/terraform/provider.go index b67a72f4c..d92c3d0b0 100644 --- a/pkg/remote/terraform/provider.go +++ b/pkg/remote/terraform/provider.go @@ -40,14 +40,16 @@ type TerraformProvider struct { schemas map[string]providers.Schema Config TerraformProviderConfig runner *parallel.ParallelRunner + progress output.Progress } -func NewTerraformProvider(installer *tf.ProviderInstaller, config TerraformProviderConfig) (*TerraformProvider, error) { +func NewTerraformProvider(installer *tf.ProviderInstaller, config TerraformProviderConfig, progress output.Progress) (*TerraformProvider, error) { p := TerraformProvider{ providerInstaller: installer, runner: parallel.NewParallelRunner(context.TODO(), 10), grpcProviders: make(map[string]*plugin.GRPCProvider), Config: config, + progress: progress, } return &p, nil } @@ -203,6 +205,7 @@ func (p *TerraformProvider) ReadResource(args tf.ReadResourceArgs) (*cty.Value, if err != nil { return nil, err } + p.progress.Inc() return &newState, nil }