diff --git a/pkg/cmd/scan.go b/pkg/cmd/scan.go index 3cf3833d..54bcfd96 100644 --- a/pkg/cmd/scan.go +++ b/pkg/cmd/scan.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + "github.com/cloudskiff/driftctl/pkg/memstore" "github.com/cloudskiff/driftctl/pkg/remote/common" "github.com/cloudskiff/driftctl/pkg/telemetry" "github.com/fatih/color" @@ -179,6 +180,7 @@ func NewScanCmd() *cobra.Command { } func scanRun(opts *pkg.ScanOptions) error { + store := memstore.New() selectedOutput := output.GetOutput(opts.Output, opts.Quiet) c := make(chan os.Signal) @@ -216,7 +218,7 @@ func scanRun(opts *pkg.ScanOptions) error { return err } - ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts, scanProgress, iacProgress, resourceSchemaRepository) + ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts, scanProgress, iacProgress, resourceSchemaRepository, store) go func() { <-c @@ -237,7 +239,7 @@ func scanRun(opts *pkg.ScanOptions) error { globaloutput.Printf(color.WhiteString("Scan duration: %s\n", analysis.Duration.Round(time.Second))) if !opts.DisableTelemetry { - telemetry.SendTelemetry(analysis) + telemetry.SendTelemetry(store) } globaloutput.Printf(color.WhiteString("Provider version used to scan: %s. Use --tf-provider-version to use another version.\n"), resourceSchemaRepository.ProviderVersion.String()) diff --git a/pkg/driftctl.go b/pkg/driftctl.go index f1d96e7d..012525b2 100644 --- a/pkg/driftctl.go +++ b/pkg/driftctl.go @@ -2,9 +2,12 @@ package pkg import ( "fmt" + "runtime" "time" + "github.com/cloudskiff/driftctl/pkg/memstore" globaloutput "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/version" "github.com/jmespath/go-jmespath" "github.com/sirupsen/logrus" @@ -45,6 +48,7 @@ type DriftCTL struct { iacProgress globaloutput.Progress resourceSchemaRepository resource.SchemaRepositoryInterface opts *ScanOptions + s memstore.Store } func NewDriftCTL(remoteSupplier resource.Supplier, @@ -54,7 +58,8 @@ func NewDriftCTL(remoteSupplier resource.Supplier, opts *ScanOptions, scanProgress globaloutput.Progress, iacProgress globaloutput.Progress, - resourceSchemaRepository resource.SchemaRepositoryInterface) *DriftCTL { + resourceSchemaRepository resource.SchemaRepositoryInterface, + store memstore.Store) *DriftCTL { return &DriftCTL{ remoteSupplier, iacSupplier, @@ -65,6 +70,7 @@ func NewDriftCTL(remoteSupplier resource.Supplier, iacProgress, resourceSchemaRepository, opts, + store, } } @@ -138,6 +144,13 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) { analysis.Duration = time.Since(start) analysis.Date = time.Now() + d.s.Bucket(memstore.TelemetryBucket).Set("version", version.Current()) + d.s.Bucket(memstore.TelemetryBucket).Set("os", runtime.GOOS) + d.s.Bucket(memstore.TelemetryBucket).Set("arch", runtime.GOARCH) + d.s.Bucket(memstore.TelemetryBucket).Set("total_resources", analysis.Summary().TotalResources) + d.s.Bucket(memstore.TelemetryBucket).Set("total_managed", analysis.Summary().TotalManaged) + d.s.Bucket(memstore.TelemetryBucket).Set("duration", uint(analysis.Duration.Seconds()+0.5)) + return &analysis, nil } diff --git a/pkg/driftctl_test.go b/pkg/driftctl_test.go index a132dd0e..cfb9f407 100644 --- a/pkg/driftctl_test.go +++ b/pkg/driftctl_test.go @@ -8,6 +8,7 @@ import ( "github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/analyser" "github.com/cloudskiff/driftctl/pkg/filter" + "github.com/cloudskiff/driftctl/pkg/memstore" "github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource/aws" @@ -98,7 +99,7 @@ func runTest(t *testing.T, cases TestCases) { iacProgress.On("Start").Return().Once() iacProgress.On("Stop").Return().Once() - driftctl := pkg.NewDriftCTL(remoteSupplier, stateSupplier, testAlerter, resourceFactory, c.options, scanProgress, iacProgress, repo) + driftctl := pkg.NewDriftCTL(remoteSupplier, stateSupplier, testAlerter, resourceFactory, c.options, scanProgress, iacProgress, repo, memstore.New()) analysis, err := driftctl.Run() diff --git a/pkg/memstore/buckets.go b/pkg/memstore/buckets.go new file mode 100644 index 00000000..3868a642 --- /dev/null +++ b/pkg/memstore/buckets.go @@ -0,0 +1,6 @@ +package memstore + +const ( + // TelemetryBucket is the name of the store bucket used by the telemetry service + TelemetryBucket = "telemetry" +) diff --git a/pkg/memstore/memstore.go b/pkg/memstore/memstore.go new file mode 100644 index 00000000..df61236b --- /dev/null +++ b/pkg/memstore/memstore.go @@ -0,0 +1,63 @@ +package memstore + +import ( + "encoding/json" + "sync" +) + +type Bucket interface { + Set(string, interface{}) + Get(string) interface{} + MarshallJSON() ([]byte, error) +} + +type Store interface { + Bucket(string) Bucket +} + +type store struct { + m *sync.Mutex + s map[string]*bucket +} + +type bucket struct { + m *sync.RWMutex + s map[string]interface{} +} + +func New() Store { + return &store{ + m: &sync.Mutex{}, + s: map[string]*bucket{}, + } +} + +func (s store) Bucket(name string) Bucket { + s.m.Lock() + defer s.m.Unlock() + + if _, ok := s.s[name]; !ok { + s.s[name] = &bucket{ + m: &sync.RWMutex{}, + s: map[string]interface{}{}, + } + } + + return s.s[name] +} + +func (b bucket) Set(key string, value interface{}) { + b.m.Lock() + defer b.m.Unlock() + b.s[key] = value +} + +func (b bucket) Get(key string) interface{} { + b.m.RLock() + defer b.m.RUnlock() + return b.s[key] +} + +func (b bucket) MarshallJSON() ([]byte, error) { + return json.Marshal(b.s) +} diff --git a/pkg/memstore/memstore_test.go b/pkg/memstore/memstore_test.go new file mode 100644 index 00000000..e1886992 --- /dev/null +++ b/pkg/memstore/memstore_test.go @@ -0,0 +1,70 @@ +package memstore + +import ( + "sync" + "testing" + + "github.com/cloudskiff/driftctl/test/resource" + "github.com/stretchr/testify/assert" +) + +func TestStore(t *testing.T) { + cases := []struct { + name string + bucket string + values map[string]interface{} + expectedJSON string + }{ + { + name: "test basic store usage", + bucket: "test-bucket-1", + values: map[string]interface{}{ + "test-value_|)": 13, + "duration_key": "23", + "null": nil, + "res": &resource.FakeResource{Id: "id", Type: "type"}, + }, + expectedJSON: `{"duration_key":"23","null":null,"res":{"Id":"id","Type":"type","Attrs":null},"test-value_|)":13}`, + }, + { + name: "test empty bucket", + bucket: "test-bucket-empty", + values: map[string]interface{}{}, + expectedJSON: `{}`, + }, + { + name: "test bucket with nil values", + bucket: "test-bucket-empty", + values: map[string]interface{}{ + "version": nil, + "total_resources": nil, + "total_managed": nil, + }, + expectedJSON: `{"total_managed":null,"total_resources":null,"version":null}`, + }, + } + + for _, tt := range cases { + kv := New() + + t.Run(tt.name, func(t *testing.T) { + var wg sync.WaitGroup + + for key, val := range tt.values { + wg.Add(1) + go func(key string, val interface{}, wg *sync.WaitGroup) { + defer wg.Done() + kv.Bucket(tt.bucket).Set(key, val) + assert.Equal(t, val, kv.Bucket(tt.bucket).Get(key)) + assert.Equal(t, nil, kv.Bucket("dummybucketname").Get(key)) + }(key, val, &wg) + } + + wg.Wait() + + b, err := kv.Bucket(tt.bucket).MarshallJSON() + assert.Nil(t, err) + assert.Equal(t, tt.expectedJSON, string(b)) + }) + } +} diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index fade8c97..7739ac10 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -2,40 +2,14 @@ package telemetry import ( "bytes" - "encoding/json" "net/http" - "runtime" - "github.com/cloudskiff/driftctl/pkg/analyser" - "github.com/cloudskiff/driftctl/pkg/version" + "github.com/cloudskiff/driftctl/pkg/memstore" "github.com/sirupsen/logrus" ) -type telemetry struct { - Version string `json:"version"` - Os string `json:"os"` - Arch string `json:"arch"` - TotalResources int `json:"total_resources"` - TotalManaged int `json:"total_managed"` - Duration uint `json:"duration"` -} - -func SendTelemetry(analysis *analyser.Analysis) { - - if analysis == nil { - return - } - - t := telemetry{ - Version: version.Current(), - Os: runtime.GOOS, - Arch: runtime.GOARCH, - TotalResources: analysis.Summary().TotalResources, - TotalManaged: analysis.Summary().TotalManaged, - Duration: uint(analysis.Duration.Seconds() + 0.5), - } - - body, err := json.Marshal(t) +func SendTelemetry(s memstore.Store) { + body, err := s.Bucket(memstore.TelemetryBucket).MarshallJSON() if err != nil { logrus.Debug(err) return diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go index ae3ecdb6..325aec5e 100644 --- a/pkg/telemetry/telemetry_test.go +++ b/pkg/telemetry/telemetry_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/cloudskiff/driftctl/pkg/analyser" + "github.com/cloudskiff/driftctl/pkg/memstore" "github.com/cloudskiff/driftctl/pkg/version" "github.com/cloudskiff/driftctl/test/resource" "github.com/jarcoal/httpmock" @@ -15,6 +16,14 @@ import ( ) func TestSendTelemetry(t *testing.T) { + type telemetry struct { + Version string `json:"version"` + Os string `json:"os"` + Arch string `json:"arch"` + TotalResources int `json:"total_resources"` + TotalManaged int `json:"total_managed"` + Duration uint `json:"duration"` + } httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -64,6 +73,17 @@ func TestSendTelemetry(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + s := memstore.New() + + if tt.analysis != nil { + s.Bucket(memstore.TelemetryBucket).Set("version", version.Current()) + s.Bucket(memstore.TelemetryBucket).Set("os", runtime.GOOS) + s.Bucket(memstore.TelemetryBucket).Set("arch", runtime.GOARCH) + s.Bucket(memstore.TelemetryBucket).Set("total_resources", tt.analysis.Summary().TotalResources) + s.Bucket(memstore.TelemetryBucket).Set("total_managed", tt.analysis.Summary().TotalManaged) + s.Bucket(memstore.TelemetryBucket).Set("duration", uint(tt.analysis.Duration.Seconds()+0.5)) + } + httpmock.Reset() if tt.expectedBody != nil { httpmock.RegisterResponder( @@ -91,7 +111,7 @@ func TestSendTelemetry(t *testing.T) { }, ) } - SendTelemetry(tt.analysis) + SendTelemetry(s) }) } }