Add progress service that display a spinner until stopped or timeouted

main
Martin Guibert 2021-03-15 18:30:18 +01:00
parent 23c4fcff67
commit 28d3a6df7e
16 changed files with 247 additions and 29 deletions

View File

@ -66,7 +66,7 @@ deps:
.PHONY: install-tools .PHONY: install-tools
install-tools: install-tools:
$(GOGET) gotest.tools/gotestsum $(GOGET) gotest.tools/gotestsum
$(GOGET) github.com/vektra/mockery/.../ $(GOGET) github.com/vektra/mockery/v2/.../
go.mod: FORCE go.mod: FORCE

View File

@ -20,6 +20,7 @@ import (
"github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/iac/config"
"github.com/cloudskiff/driftctl/pkg/iac/supplier" "github.com/cloudskiff/driftctl/pkg/iac/supplier"
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend" "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/remote"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
@ -32,6 +33,7 @@ type ScanOptions struct {
To string To string
Output output.OutputConfig Output output.OutputConfig
Filter *jmespath.JMESPath Filter *jmespath.JMESPath
Quiet bool
} }
func NewScanCmd() *cobra.Command { func NewScanCmd() *cobra.Command {
@ -77,6 +79,8 @@ func NewScanCmd() *cobra.Command {
opts.Filter = expr opts.Filter = expr
} }
opts.Quiet, _ = cmd.Flags().GetBool("quiet")
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
@ -85,6 +89,12 @@ func NewScanCmd() *cobra.Command {
} }
fl := cmd.Flags() fl := cmd.Flags()
fl.BoolP(
"quiet",
"",
false,
"Do not display anything but scan results",
)
fl.StringP( fl.StringP(
"filter", "filter",
"", "",
@ -123,7 +133,7 @@ func NewScanCmd() *cobra.Command {
} }
func scanRun(opts *ScanOptions) error { func scanRun(opts *ScanOptions) error {
selectedOutput := output.GetOutput(opts.Output) 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)
@ -132,7 +142,9 @@ func scanRun(opts *ScanOptions) error {
providerLibrary := terraform.NewProviderLibrary() providerLibrary := terraform.NewProviderLibrary()
supplierLibrary := resource.NewSupplierLibrary() 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 { if err != nil {
return err return err
} }
@ -158,7 +170,9 @@ func scanRun(opts *ScanOptions) error {
ctl.Stop() ctl.Stop()
}() }()
progress.Start()
analysis, err := ctl.Run() analysis, err := ctl.Run()
progress.Stop()
if err != nil { if err != nil {
return err return err

View File

@ -47,8 +47,8 @@ func IsSupported(key string) bool {
return false return false
} }
func GetOutput(config OutputConfig) Output { func GetOutput(config OutputConfig, quiet bool) Output {
output.ChangePrinter(GetPrinter(config)) output.ChangePrinter(GetPrinter(config, quiet))
switch config.Key { switch config.Key {
case JSONOutputType: 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 { switch config.Key {
case JSONOutputType: case JSONOutputType:
if isStdOut(config.Options["path"]) { if isStdOut(config.Options["path"]) {

View File

@ -267,10 +267,11 @@ func fakeAnalysisWithGithubEnumerationError() *analyser.Analysis {
func TestGetPrinter(t *testing.T) { func TestGetPrinter(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
key string key string
want output.Printer quiet bool
want output.Printer
}{ }{
{ {
name: "json file output", name: "json file output",
@ -278,6 +279,13 @@ func TestGetPrinter(t *testing.T) {
key: JSONOutputType, key: JSONOutputType,
want: output.NewConsolePrinter(), want: output.NewConsolePrinter(),
}, },
{
name: "json file output quiet",
path: "/path/to/file",
key: JSONOutputType,
quiet: true,
want: &output.VoidPrinter{},
},
{ {
name: "json stdout output", name: "json stdout output",
path: "stdout", path: "stdout",
@ -296,6 +304,13 @@ func TestGetPrinter(t *testing.T) {
key: ConsoleOutputType, key: ConsoleOutputType,
want: output.NewConsolePrinter(), want: output.NewConsolePrinter(),
}, },
{
name: "quiet console stdout output",
path: "stdout",
quiet: true,
key: ConsoleOutputType,
want: &output.VoidPrinter{},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -304,7 +319,7 @@ func TestGetPrinter(t *testing.T) {
Options: map[string]string{ Options: map[string]string{
"path": tt.path, "path": tt.path,
}, },
}); !reflect.DeepEqual(got, tt.want) { }, tt.quiet); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPrinter() = %v, want %v", got, tt.want) t.Errorf("GetPrinter() = %v, want %v", got, tt.want)
} }
}) })

View File

@ -7,6 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/cloudskiff/driftctl/pkg/output"
"github.com/cloudskiff/driftctl/pkg/iac" "github.com/cloudskiff/driftctl/pkg/iac"
"github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/iac/config"
"github.com/cloudskiff/driftctl/pkg/remote/aws" "github.com/cloudskiff/driftctl/pkg/remote/aws"
@ -96,7 +98,9 @@ func TestTerraformStateReader_AWS_Resources(t *testing.T) {
if shouldUpdate { if shouldUpdate {
var err error var err error
realProvider, err = aws.NewAWSTerraformProvider() progress := &output.MockProgress{}
progress.On("Inc").Return()
realProvider, err = aws.NewAWSTerraformProvider(progress)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -171,7 +175,9 @@ func TestTerraformStateReader_Github_Resources(t *testing.T) {
if shouldUpdate { if shouldUpdate {
var err error var err error
realProvider, err = github.NewGithubTerraformProvider() progress := &output.MockProgress{}
progress.On("Inc").Return()
realProvider, err = github.NewGithubTerraformProvider(progress)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -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
}

98
pkg/output/progress.go Normal file
View File

@ -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{}{}
}

View File

@ -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())
}

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"github.com/cloudskiff/driftctl/pkg/alerter" "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/client"
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository" "github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
"github.com/cloudskiff/driftctl/pkg/resource" "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) * Initialize remote (configure credentials, launch tf providers and start gRPC clients)
* Required to use Scanner * Required to use Scanner
*/ */
func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary) error { func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error {
provider, err := NewAWSTerraformProvider() provider, err := NewAWSTerraformProvider(progress)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,9 +1,12 @@
package aws 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) { func InitTestAwsProvider(providerLibrary *terraform.ProviderLibrary) (*AWSTerraformProvider, error) {
provider, err := NewAWSTerraformProvider() provider, err := NewAWSTerraformProvider(&output.MockProgress{})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,6 +3,7 @@ package aws
import ( import (
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/cloudskiff/driftctl/pkg/output"
"github.com/cloudskiff/driftctl/pkg/remote/terraform" "github.com/cloudskiff/driftctl/pkg/remote/terraform"
tf "github.com/cloudskiff/driftctl/pkg/terraform" tf "github.com/cloudskiff/driftctl/pkg/terraform"
) )
@ -41,7 +42,7 @@ type AWSTerraformProvider struct {
session *session.Session session *session.Session
} }
func NewAWSTerraformProvider() (*AWSTerraformProvider, error) { func NewAWSTerraformProvider(progress output.Progress) (*AWSTerraformProvider, error) {
p := &AWSTerraformProvider{} p := &AWSTerraformProvider{}
providerKey := "aws" providerKey := "aws"
installer, err := tf.NewProviderInstaller(tf.ProviderConfig{ installer, err := tf.NewProviderInstaller(tf.ProviderConfig{
@ -64,7 +65,7 @@ func NewAWSTerraformProvider() (*AWSTerraformProvider, error) {
MaxRetries: 10, // TODO make this configurable MaxRetries: 10, // TODO make this configurable
} }
}, },
}) }, progress)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,6 +2,7 @@ package github
import ( import (
"github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/output"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform" "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) * Initialize remote (configure credentials, launch tf providers and start gRPC clients)
* Required to use Scanner * Required to use Scanner
*/ */
func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary) error { func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error {
provider, err := NewGithubTerraformProvider() provider, err := NewGithubTerraformProvider(progress)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,9 +1,12 @@
package github 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) { func InitTestGithubProvider(providerLibrary *terraform.ProviderLibrary) (*GithubTerraformProvider, error) {
provider, err := NewGithubTerraformProvider() provider, err := NewGithubTerraformProvider(&output.MockProgress{})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,6 +3,8 @@ package github
import ( import (
"os" "os"
"github.com/cloudskiff/driftctl/pkg/output"
"github.com/cloudskiff/driftctl/pkg/remote/terraform" "github.com/cloudskiff/driftctl/pkg/remote/terraform"
tf "github.com/cloudskiff/driftctl/pkg/terraform" tf "github.com/cloudskiff/driftctl/pkg/terraform"
) )
@ -17,7 +19,7 @@ type githubConfig struct {
Organization string Organization string
} }
func NewGithubTerraformProvider() (*GithubTerraformProvider, error) { func NewGithubTerraformProvider(progress output.Progress) (*GithubTerraformProvider, error) {
p := &GithubTerraformProvider{} p := &GithubTerraformProvider{}
providerKey := "github" providerKey := "github"
installer, err := tf.NewProviderInstaller(tf.ProviderConfig{ installer, err := tf.NewProviderInstaller(tf.ProviderConfig{
@ -35,7 +37,7 @@ func NewGithubTerraformProvider() (*GithubTerraformProvider, error) {
Owner: p.GetConfig().getDefaultOwner(), Owner: p.GetConfig().getDefaultOwner(),
} }
}, },
}) }, progress)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,6 +2,7 @@ package remote
import ( import (
"github.com/cloudskiff/driftctl/pkg/alerter" "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/aws"
"github.com/cloudskiff/driftctl/pkg/remote/github" "github.com/cloudskiff/driftctl/pkg/remote/github"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
@ -23,12 +24,12 @@ func IsSupported(remote string) bool {
return false 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 { switch remote {
case aws.RemoteAWSTerraform: case aws.RemoteAWSTerraform:
return aws.Init(alerter, providerLibrary, supplierLibrary) return aws.Init(alerter, providerLibrary, supplierLibrary, progress)
case github.RemoteGithubTerraform: case github.RemoteGithubTerraform:
return github.Init(alerter, providerLibrary, supplierLibrary) return github.Init(alerter, providerLibrary, supplierLibrary, progress)
default: default:
return errors.Errorf("unsupported remote '%s'", remote) return errors.Errorf("unsupported remote '%s'", remote)
} }

View File

@ -40,14 +40,16 @@ type TerraformProvider struct {
schemas map[string]providers.Schema schemas map[string]providers.Schema
Config TerraformProviderConfig Config TerraformProviderConfig
runner *parallel.ParallelRunner 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{ p := TerraformProvider{
providerInstaller: installer, providerInstaller: installer,
runner: parallel.NewParallelRunner(context.TODO(), 10), runner: parallel.NewParallelRunner(context.TODO(), 10),
grpcProviders: make(map[string]*plugin.GRPCProvider), grpcProviders: make(map[string]*plugin.GRPCProvider),
Config: config, Config: config,
progress: progress,
} }
return &p, nil return &p, nil
} }
@ -203,6 +205,7 @@ func (p *TerraformProvider) ReadResource(args tf.ReadResourceArgs) (*cty.Value,
if err != nil { if err != nil {
return nil, err return nil, err
} }
p.progress.Inc()
return &newState, nil return &newState, nil
} }