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
install-tools:
$(GOGET) gotest.tools/gotestsum
$(GOGET) github.com/vektra/mockery/.../
$(GOGET) github.com/vektra/mockery/v2/.../
go.mod: FORCE

View File

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

View File

@ -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"]) {

View File

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

View File

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

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 (
"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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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