Add progress service that display a spinner until stopped or timeouted
parent
23c4fcff67
commit
28d3a6df7e
2
Makefile
2
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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{}{}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue