Add alerting

main
William Beuil 2020-12-16 13:02:02 +01:00
parent dafa22e99e
commit 8e5b6de9b2
No known key found for this signature in database
GPG Key ID: BED2072C5C2BF537
21 changed files with 954 additions and 22 deletions

8
pkg/alerter/alert.go Normal file
View File

@ -0,0 +1,8 @@
package alerter
type Alerts map[string][]Alert
type Alert struct {
Message string `json:"message"`
ShouldIgnoreResource bool `json:"-"`
}

71
pkg/alerter/alerter.go Normal file
View File

@ -0,0 +1,71 @@
package alerter
import (
"fmt"
"github.com/cloudskiff/driftctl/pkg/resource"
)
type Alerter struct {
alerts Alerts
alertsCh chan Alerts
doneCh chan bool
}
func NewAlerter() *Alerter {
var alerter = &Alerter{
alerts: make(Alerts),
alertsCh: make(chan Alerts),
doneCh: make(chan bool),
}
go alerter.run()
return alerter
}
func (a *Alerter) run() {
defer func() { a.doneCh <- true }()
for alert := range a.alertsCh {
for k, v := range alert {
if val, ok := a.alerts[k]; ok {
a.alerts[k] = append(val, v...)
} else {
a.alerts[k] = v
}
}
}
}
func (a *Alerter) SetAlerts(alerts Alerts) {
a.alerts = alerts
}
func (a *Alerter) GetAlerts() Alerts {
close(a.alertsCh)
<-a.doneCh
return a.alerts
}
func (a *Alerter) SendAlert(key string, alert Alert) {
a.alertsCh <- Alerts{
key: []Alert{alert},
}
}
func (a *Alerter) IsResourceIgnored(res resource.Resource) bool {
alert, alertExists := a.alerts[fmt.Sprintf("%s.%s", res.TerraformType(), res.TerraformId())]
wildcardAlert, wildcardAlertExists := a.alerts[res.TerraformType()]
shouldIgnoreAlert := a.shouldBeIgnored(alert)
shouldIgnoreWildcardAlert := a.shouldBeIgnored(wildcardAlert)
return (alertExists && shouldIgnoreAlert) || (wildcardAlertExists && shouldIgnoreWildcardAlert)
}
func (a *Alerter) shouldBeIgnored(alert []Alert) bool {
for _, a := range alert {
if a.ShouldIgnoreResource {
return true
}
}
return false
}

210
pkg/alerter/alerter_test.go Normal file
View File

@ -0,0 +1,210 @@
package alerter
import (
"reflect"
"testing"
"github.com/cloudskiff/driftctl/pkg/resource"
resource2 "github.com/cloudskiff/driftctl/test/resource"
)
func TestAlerter_Alert(t *testing.T) {
cases := []struct {
name string
alerts Alerts
expected Alerts
}{
{
name: "TestNoAlerts",
alerts: nil,
expected: Alerts{},
},
{
name: "TestWithSingleAlert",
alerts: Alerts{
"fakeres.foobar": []Alert{
{
Message: "This is an alert",
ShouldIgnoreResource: false,
},
},
},
expected: Alerts{
"fakeres.foobar": []Alert{
{
Message: "This is an alert",
ShouldIgnoreResource: false,
},
},
},
},
{
name: "TestWithMultipleAlerts",
alerts: Alerts{
"fakeres.foobar": []Alert{
{
Message: "This is an alert",
ShouldIgnoreResource: false,
},
{
Message: "This is a second alert",
ShouldIgnoreResource: true,
},
},
"fakeres.barfoo": []Alert{
{
Message: "This is a third alert",
ShouldIgnoreResource: true,
},
},
},
expected: Alerts{
"fakeres.foobar": []Alert{
{
Message: "This is an alert",
ShouldIgnoreResource: false,
},
{
Message: "This is a second alert",
ShouldIgnoreResource: true,
},
},
"fakeres.barfoo": []Alert{
{
Message: "This is a third alert",
ShouldIgnoreResource: true,
},
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
alerter := NewAlerter()
for k, v := range c.alerts {
for _, a := range v {
alerter.SendAlert(k, a)
}
}
if eq := reflect.DeepEqual(alerter.GetAlerts(), c.expected); !eq {
t.Errorf("Got %+v, expected %+v", alerter.GetAlerts(), c.expected)
}
})
}
}
func TestAlerter_IgnoreResources(t *testing.T) {
cases := []struct {
name string
alerts Alerts
resource resource.Resource
expected bool
}{
{
name: "TestNoAlerts",
alerts: Alerts{},
resource: &resource2.FakeResource{
Type: "fakeres",
Id: "foobar",
},
expected: false,
},
{
name: "TestShouldNotBeIgnoredWithAlerts",
alerts: Alerts{
"fakeres": {
{
Message: "Should not be ignored",
},
},
"fakeres.foobar": {
{
Message: "Should not be ignored",
},
},
"fakeres.barfoo": {
{
Message: "Should not be ignored",
},
},
"other.resource": {
{
Message: "Should not be ignored",
},
},
},
resource: &resource2.FakeResource{
Type: "fakeres",
Id: "foobar",
},
expected: false,
},
{
name: "TestShouldBeIgnoredWithAlertsOnWildcard",
alerts: Alerts{
"fakeres": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.foobaz": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.resource": {
{
Message: "Should not be ignored",
},
},
},
resource: &resource2.FakeResource{
Type: "fakeres",
Id: "foobar",
},
expected: true,
},
{
name: "TestShouldBeIgnoredWithAlertsOnResource",
alerts: Alerts{
"fakeres": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.foobaz": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.resource": {
{
Message: "Should not be ignored",
},
},
},
resource: &resource2.FakeResource{
Type: "other",
Id: "foobaz",
},
expected: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
alerter := NewAlerter()
alerter.SetAlerts(c.alerts)
if got := alerter.IsResourceIgnored(c.resource); got != c.expected {
t.Errorf("Got %+v, expected %+v", got, c.expected)
}
})
}
}

View File

@ -3,6 +3,7 @@ package analyser
import (
"encoding/json"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/r3labs/diff/v2"
)
@ -26,6 +27,7 @@ type Analysis struct {
deleted []resource.Resource
differences []Difference
summary Summary
alerts alerter.Alerts
}
type serializableDifference struct {
@ -40,6 +42,7 @@ type serializableAnalysis struct {
Deleted []resource.SerializableResource `json:"deleted"`
Differences []serializableDifference `json:"differences"`
Coverage int `json:"coverage"`
Alerts alerter.Alerts `json:"alerts"`
}
func (a Analysis) MarshalJSON() ([]byte, error) {
@ -61,6 +64,7 @@ func (a Analysis) MarshalJSON() ([]byte, error) {
}
bla.Summary = a.summary
bla.Coverage = a.Coverage()
bla.Alerts = a.alerts
return json.Marshal(bla)
}
@ -97,6 +101,7 @@ func (a *Analysis) UnmarshalJSON(bytes []byte) error {
Changelog: di.Changelog,
})
}
a.AddAlerts(bla.Alerts)
return nil
}
@ -127,6 +132,10 @@ func (a *Analysis) AddDifference(diffs ...Difference) {
a.summary.TotalDrifted += len(diffs)
}
func (a *Analysis) AddAlerts(alerts alerter.Alerts) {
a.alerts = alerts
}
func (a *Analysis) Coverage() int {
if a.summary.TotalResources > 0 {
return int((float32(a.summary.TotalManaged) / float32(a.summary.TotalResources)) * 100.0)
@ -153,3 +162,7 @@ func (a *Analysis) Differences() []Difference {
func (a *Analysis) Summary() Summary {
return a.summary
}
func (a *Analysis) Alerts() alerter.Alerts {
return a.alerts
}

View File

@ -1,30 +1,36 @@
package analyser
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/r3labs/diff/v2"
)
type Analyzer struct{}
type Analyzer struct {
alerter *alerter.Alerter
}
type Filter interface {
IsResourceIgnored(res resource.Resource) bool
IsFieldIgnored(res resource.Resource, path []string) bool
}
func NewAnalyzer() Analyzer {
return Analyzer{}
func NewAnalyzer(alerter *alerter.Alerter) Analyzer {
return Analyzer{alerter}
}
func (a Analyzer) Analyze(remoteResources []resource.Resource, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) {
func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) {
analysis := Analysis{}
// Iterate on remote resources and filter ignored resources
filteredRemoteResource := make([]resource.Resource, 0, len(remoteResources))
for _, remoteRes := range remoteResources {
if filter.IsResourceIgnored(remoteRes) {
if filter.IsResourceIgnored(remoteRes) || a.alerter.IsResourceIgnored(remoteRes) {
continue
}
filteredRemoteResource = append(filteredRemoteResource, remoteRes)
@ -33,7 +39,7 @@ func (a Analyzer) Analyze(remoteResources []resource.Resource, resourcesFromStat
for _, stateRes := range resourcesFromState {
i, remoteRes, found := findCorrespondingRes(filteredRemoteResource, stateRes)
if filter.IsResourceIgnored(stateRes) {
if filter.IsResourceIgnored(stateRes) || a.alerter.IsResourceIgnored(stateRes) {
continue
}
@ -63,6 +69,7 @@ func (a Analyzer) Analyze(remoteResources []resource.Resource, resourcesFromStat
Res: stateRes,
Changelog: changelog,
})
a.sendAlertOnComputedField(stateRes, delta)
}
}
}
@ -70,6 +77,8 @@ func (a Analyzer) Analyze(remoteResources []resource.Resource, resourcesFromStat
// Add remaining unmanaged resources
analysis.AddUnmanaged(filteredRemoteResource...)
analysis.AddAlerts(a.alerter.GetAlerts())
return analysis, nil
}
@ -88,3 +97,51 @@ func removeResourceByIndex(i int, resources []resource.Resource) []resource.Reso
}
return append(resources[:i], resources[i+1:]...)
}
// sendAlertOnComputedField will send an alert to a channel if the field of a delta (e.g. diff)
// has a computed tag
func (a Analyzer) sendAlertOnComputedField(stateRes resource.Resource, delta diff.Changelog) {
for _, d := range delta {
if field, ok := a.getField(reflect.TypeOf(stateRes), d.Path); ok {
if computed := field.Tag.Get("computed") == "true"; computed {
path := strings.Join(d.Path, ".")
a.alerter.SendAlert(fmt.Sprintf("%s.%s", stateRes.TerraformType(), stateRes.TerraformId()),
alerter.Alert{
Message: fmt.Sprintf("%s is a computed field", path),
})
}
}
}
}
// getField recursively finds the deepest field inside a resource depending on
// its path and its type
func (a Analyzer) getField(t reflect.Type, path []string) (reflect.StructField, bool) {
switch t.Kind() {
case reflect.Ptr:
return a.getField(t.Elem(), path)
case reflect.Slice:
return a.getField(t.Elem(), path[1:])
default:
{
if field, ok := t.FieldByName(path[0]); ok && a.hasNestedFields(field.Type) {
return a.getField(field.Type, path[1:])
} else {
return field, ok
}
}
}
}
// hasNestedFields will return true if the current field is either a struct
// or a slice of struct
func (a Analyzer) hasNestedFields(t reflect.Type) bool {
switch t.Kind() {
case reflect.Ptr:
return a.hasNestedFields(t.Elem())
case reflect.Slice:
return t.Elem().Kind() == reflect.Struct
default:
return t.Kind() == reflect.Struct
}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/cloudskiff/driftctl/test/goldenfile"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/r3labs/diff/v2"
@ -30,6 +31,7 @@ func TestAnalyze(t *testing.T) {
res resource.Resource
path []string
}
alerts alerter.Alerts
expected Analysis
hasDrifted bool
}{
@ -174,6 +176,10 @@ func TestAnalyze(t *testing.T) {
Id: "foobar",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
},
cloud: []resource.Resource{
@ -181,6 +187,10 @@ func TestAnalyze(t *testing.T) {
Id: "foobar",
FooBar: "barfoo",
BarFoo: "foobar",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"bar", "baz"},
},
},
expected: Analysis{
@ -189,6 +199,10 @@ func TestAnalyze(t *testing.T) {
Id: "foobar",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
},
summary: Summary{
@ -202,6 +216,10 @@ func TestAnalyze(t *testing.T) {
Id: "foobar",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
Changelog: diff.Changelog{
diff.Change{
@ -220,6 +238,34 @@ func TestAnalyze(t *testing.T) {
"BarFoo",
},
},
diff.Change{
Type: "update",
From: "baz",
To: "bar",
Path: []string{
"Struct",
"Baz",
},
},
diff.Change{
Type: "update",
From: "bar",
To: "baz",
Path: []string{
"Struct",
"Bar",
},
},
},
},
},
alerts: alerter.Alerts{
"FakeResource.foobar": {
{
Message: "BarFoo is a computed field",
},
{
Message: "Struct.Baz is a computed field",
},
},
},
@ -231,6 +277,7 @@ func TestAnalyze(t *testing.T) {
iac: []resource.Resource{
&testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
},
@ -238,6 +285,7 @@ func TestAnalyze(t *testing.T) {
cloud: []resource.Resource{
&testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "barfoo",
BarFoo: "foobar",
},
@ -249,6 +297,7 @@ func TestAnalyze(t *testing.T) {
{
res: &testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
},
@ -259,6 +308,7 @@ func TestAnalyze(t *testing.T) {
managed: []resource.Resource{
&testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
},
@ -272,6 +322,7 @@ func TestAnalyze(t *testing.T) {
{
Res: &testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
},
@ -287,6 +338,13 @@ func TestAnalyze(t *testing.T) {
},
},
},
alerts: alerter.Alerts{
"fakeres.foobar": {
{
Message: "BarFoo is a computed field",
},
},
},
},
hasDrifted: true,
},
@ -351,10 +409,260 @@ func TestAnalyze(t *testing.T) {
},
hasDrifted: false,
},
{
name: "TestDiffWithAlertFiltering",
iac: []resource.Resource{
&testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
&testresource.FakeResource{
Id: "barfoo",
Type: "fakeres",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
&testresource.FakeResource{
Id: "foobaz",
Type: "other",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
},
&testresource.FakeResource{
Id: "resource",
Type: "other",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
StructSlice: []struct {
String string `computed:"true"`
Array []string `computed:"true"`
}{
{"one", []string{"foo"}},
},
},
},
cloud: []resource.Resource{
&testresource.FakeResource{
Id: "foobar",
Type: "fakeres",
FooBar: "barfoo",
BarFoo: "foobar",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"bar", "baz"},
},
&testresource.FakeResource{
Id: "barfoo",
Type: "fakeres",
FooBar: "barfoo",
BarFoo: "foobar",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"bar", "baz"},
},
&testresource.FakeResource{
Id: "foobaz",
Type: "other",
FooBar: "barfoo",
BarFoo: "foobar",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"bar", "baz"},
},
&testresource.FakeResource{
Id: "resource",
Type: "other",
FooBar: "barfoo",
BarFoo: "foobar",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"bar", "baz"},
StructSlice: []struct {
String string `computed:"true"`
Array []string `computed:"true"`
}{
{"two", []string{"oof"}},
},
},
},
alerts: alerter.Alerts{
"fakeres": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.foobaz": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.resource": {
{
Message: "Should not be ignored",
},
},
},
expected: Analysis{
managed: []resource.Resource{
&testresource.FakeResource{
Id: "resource",
Type: "other",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
StructSlice: []struct {
String string `computed:"true"`
Array []string `computed:"true"`
}{
{"one", []string{"foo"}},
},
},
},
summary: Summary{
TotalResources: 1,
TotalDrifted: 1,
TotalManaged: 1,
},
differences: []Difference{
{
Res: &testresource.FakeResource{
Id: "resource",
Type: "other",
FooBar: "foobar",
BarFoo: "barfoo",
Struct: struct {
Baz string `computed:"true"`
Bar string
}{"baz", "bar"},
StructSlice: []struct {
String string `computed:"true"`
Array []string `computed:"true"`
}{
{"one", []string{"foo"}},
},
},
Changelog: diff.Changelog{
diff.Change{
Type: "update",
From: "foobar",
To: "barfoo",
Path: []string{
"FooBar",
},
},
diff.Change{
Type: "update",
From: "barfoo",
To: "foobar",
Path: []string{
"BarFoo",
},
},
diff.Change{
Type: "update",
From: "baz",
To: "bar",
Path: []string{
"Struct",
"Baz",
},
},
diff.Change{
Type: "update",
From: "bar",
To: "baz",
Path: []string{
"Struct",
"Bar",
},
},
diff.Change{
Type: "update",
From: "foo",
To: "oof",
Path: []string{
"StructSlice",
"0",
"Array",
"0",
},
},
diff.Change{
Type: "update",
From: "one",
To: "two",
Path: []string{
"StructSlice",
"0",
"String",
},
},
},
},
},
alerts: alerter.Alerts{
"fakeres": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.foobaz": {
{
Message: "Should be ignored",
ShouldIgnoreResource: true,
},
},
"other.resource": {
{
Message: "Should not be ignored",
},
{
Message: "BarFoo is a computed field",
},
{
Message: "Struct.Baz is a computed field",
},
{
Message: "StructSlice.0.String is a computed field",
},
{
Message: "StructSlice.0.Array.0 is a computed field",
},
},
},
},
hasDrifted: true,
},
}
analyzer := NewAnalyzer()
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
filter := &mocks.Filter{}
@ -368,6 +676,12 @@ func TestAnalyze(t *testing.T) {
}
filter.On("IsFieldIgnored", mock.Anything, mock.Anything).Return(false)
alerter := alerter.NewAlerter()
if c.alerts != nil {
alerter.SetAlerts(c.alerts)
}
analyzer := NewAnalyzer(alerter)
result, err := analyzer.Analyze(c.cloud, c.iac, filter)
if err != nil {
@ -429,6 +743,15 @@ func TestAnalyze(t *testing.T) {
}
}
alertsChanges, err := diff.Diff(result.Alerts(), c.expected.Alerts())
if err != nil {
t.Fatalf("Unable to compare %+v", err)
}
if len(alertsChanges) > 0 {
for _, change := range alertsChanges {
t.Errorf("%+v", change)
}
}
})
}
}
@ -479,6 +802,13 @@ func TestAnalysis_MarshalJSON(t *testing.T) {
},
},
})
analysis.AddAlerts(alerter.Alerts{
"aws_iam_access_key": {
{
Message: "This is an alert",
},
},
})
got, err := json.MarshalIndent(analysis, "", "\t")
if err != nil {
@ -552,6 +882,13 @@ func TestAnalysis_UnmarshalJSON(t *testing.T) {
},
},
},
alerts: alerter.Alerts{
"aws_iam_access_key": {
{
Message: "This is an alert",
},
},
},
}
got := Analysis{}

View File

@ -54,5 +54,12 @@
]
}
],
"coverage": 33
"coverage": 33,
"alerts": {
"aws_iam_access_key": [
{
"message": "This is an alert"
}
]
}
}

View File

@ -54,5 +54,12 @@
]
}
],
"coverage": 33
"coverage": 33,
"alerts": {
"aws_iam_access_key": [
{
"message": "This is an alert"
}
]
}
}

View File

@ -9,6 +9,7 @@ import (
"syscall"
"github.com/cloudskiff/driftctl/pkg"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/cmd/scan/output"
"github.com/cloudskiff/driftctl/pkg/filter"
"github.com/cloudskiff/driftctl/pkg/iac/config"
@ -123,7 +124,9 @@ func scanRun(opts *ScanOptions) error {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
err := remote.Activate(opts.To)
alerter := alerter.NewAlerter()
err := remote.Activate(opts.To, alerter)
if err != nil {
return err
}
@ -135,7 +138,7 @@ func scanRun(opts *ScanOptions) error {
logrus.Trace("Exited")
}()
scanner := pkg.NewScanner(resource.Suppliers())
scanner := pkg.NewScanner(resource.Suppliers(), alerter)
iacSupplier, err := supplier.GetIACSupplier(opts.From)
if err != nil {
@ -146,7 +149,7 @@ func scanRun(opts *ScanOptions) error {
"backend": opts.From.Backend,
"path": opts.From.Path,
}).Debug("Found IAC provider")
ctl := pkg.NewDriftCTL(scanner, iacSupplier, opts.Filter)
ctl := pkg.NewDriftCTL(scanner, iacSupplier, opts.Filter, alerter)
go func() {
<-c

View File

@ -28,6 +28,7 @@ func NewConsole() *Console {
}
func (c *Console) Write(analysis *analyser.Analysis) error {
var shouldWarnOnComputedFields bool
if analysis.Summary().TotalDeleted > 0 {
fmt.Printf("Found deleted resources:\n")
@ -86,13 +87,28 @@ func (c *Console) Write(analysis *analyser.Analysis) error {
continue
}
}
fmt.Printf(" %s %s => %s\n", pref, prettify(change.From), prettify(change.To))
fmt.Printf(" %s %s => %s", pref, prettify(change.From), prettify(change.To))
for key, val := range analysis.Alerts() {
if fmt.Sprintf("%s.%s", humanString, difference.Res.TerraformId()) == key {
for _, a := range val {
if fmt.Sprintf("%s is a computed field", path) == a.Message {
shouldWarnOnComputedFields = true
fmt.Printf(" %s", color.YellowString("(computed)"))
}
}
}
}
fmt.Printf("\n")
}
}
}
c.writeSummary(analysis)
if shouldWarnOnComputedFields {
fmt.Printf("%s\n", color.YellowString("You have diffs on computed field, check the documentation for potential false positive drifts"))
}
return nil
}

View File

@ -49,6 +49,12 @@ func TestConsole_Write(t *testing.T) {
args: args{analysis: fakeAnalysisWithStringerResources()},
wantErr: false,
},
{
name: "test console output with drift on computed fields",
goldenfile: "output_computed_fields.txt",
args: args{analysis: fakeAnalysisWithComputedFields()},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -30,6 +30,14 @@ func TestJSON_Write(t *testing.T) {
},
wantErr: false,
},
{
name: "test json output with drift on computed fields",
goldenfile: "output_computed_fields.json",
args: args{
analysis: fakeAnalysisWithComputedFields(),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -3,6 +3,7 @@ package output
import (
"fmt"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/analyser"
testresource "github.com/cloudskiff/driftctl/test/resource"
"github.com/r3labs/diff/v2"
@ -148,3 +149,74 @@ func fakeAnalysisWithStringerResources() *analyser.Analysis {
}})
return &a
}
func fakeAnalysisWithComputedFields() *analyser.Analysis {
a := analyser.Analysis{}
a.AddManaged(
&testresource.FakeResource{
Id: "diff-id-1",
Type: "aws_diff_resource",
},
)
a.AddDifference(analyser.Difference{Res: testresource.FakeResource{
Id: "diff-id-1",
Type: "aws_diff_resource",
}, Changelog: []diff.Change{
{
Type: diff.UPDATE,
Path: []string{"updated", "field"},
From: "foobar",
To: "barfoo",
},
{
Type: diff.CREATE,
Path: []string{"new", "field"},
From: nil,
To: "newValue",
},
{
Type: diff.DELETE,
Path: []string{"a"},
From: "oldValue",
To: nil,
},
{
Type: diff.UPDATE,
From: "foo",
To: "oof",
Path: []string{
"struct",
"0",
"array",
"0",
},
},
{
Type: diff.UPDATE,
From: "one",
To: "two",
Path: []string{
"struct",
"0",
"string",
},
},
}})
a.AddAlerts(alerter.Alerts{
"aws_diff_resource.diff-id-1": []alerter.Alert{
{
Message: "updated.field is a computed field",
},
{
Message: "a is a computed field",
},
{
Message: "struct.0.array.0 is a computed field",
},
{
Message: "struct.0.string is a computed field",
},
},
})
return &a
}

View File

@ -72,5 +72,6 @@
]
}
],
"coverage": 33
"coverage": 33,
"alerts": null
}

View File

@ -0,0 +1,91 @@
{
"summary": {
"total_resources": 1,
"total_drifted": 1,
"total_unmanaged": 0,
"total_deleted": 0,
"total_managed": 1
},
"managed": [
{
"id": "diff-id-1",
"type": "aws_diff_resource"
}
],
"unmanaged": null,
"deleted": null,
"differences": [
{
"res": {
"id": "diff-id-1",
"type": "aws_diff_resource"
},
"changelog": [
{
"type": "update",
"path": [
"updated",
"field"
],
"from": "foobar",
"to": "barfoo"
},
{
"type": "create",
"path": [
"new",
"field"
],
"from": null,
"to": "newValue"
},
{
"type": "delete",
"path": [
"a"
],
"from": "oldValue",
"to": null
},
{
"type": "update",
"path": [
"struct",
"0",
"array",
"0"
],
"from": "foo",
"to": "oof"
},
{
"type": "update",
"path": [
"struct",
"0",
"string"
],
"from": "one",
"to": "two"
}
]
}
],
"coverage": 100,
"alerts": {
"aws_diff_resource.diff-id-1": [
{
"message": "updated.field is a computed field"
},
{
"message": "a is a computed field"
},
{
"message": "struct.0.array.0 is a computed field"
},
{
"message": "struct.0.string is a computed field"
}
]
}
}

View File

@ -0,0 +1,14 @@
Found drifted resources:
- diff-id-1 (aws_diff_resource):
~ updated.field: "foobar" => "barfoo" (computed)
+ new.field: <nil> => "newValue"
- a: "oldValue" => <nil> (computed)
~ struct.0.array.0: "foo" => "oof" (computed)
~ struct.0.string: "one" => "two" (computed)
Found 1 resource(s)
- 100% coverage
- 1 covered by IaC
- 0 not covered by IaC
- 0 deleted on cloud provider
- 1/1 drifted from IaC
You have diffs on computed field, check the documentation for potential false positive drifts

View File

@ -3,6 +3,7 @@ package pkg
import (
"fmt"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/analyser"
"github.com/cloudskiff/driftctl/pkg/filter"
"github.com/cloudskiff/driftctl/pkg/middlewares"
@ -18,8 +19,8 @@ type DriftCTL struct {
filter *jmespath.JMESPath
}
func NewDriftCTL(remoteSupplier resource.Supplier, iacSupplier resource.Supplier, filter *jmespath.JMESPath) *DriftCTL {
return &DriftCTL{remoteSupplier, iacSupplier, analyser.NewAnalyzer(), filter}
func NewDriftCTL(remoteSupplier resource.Supplier, iacSupplier resource.Supplier, filter *jmespath.JMESPath, alerter *alerter.Alerter) *DriftCTL {
return &DriftCTL{remoteSupplier, iacSupplier, analyser.NewAnalyzer(alerter), filter}
}
func (d DriftCTL) Run() *analyser.Analysis {
@ -65,6 +66,7 @@ func (d DriftCTL) Run() *analyser.Analysis {
driftIgnore := filter.NewDriftIgnore()
analysis, err := d.analyzer.Analyze(remoteResources, resourcesFromState, driftIgnore)
if err != nil {
logrus.Errorf("Unable to analyse resources: %+v", err)
return nil

View File

@ -2,6 +2,7 @@ package aws
import (
"github.com/aws/aws-sdk-go/service/iam"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform"
@ -17,7 +18,7 @@ const RemoteAWSTerraform = "aws+tf"
* Initialize remote (configure credentials, launch tf providers and start gRPC clients)
* Required to use Scanner
*/
func Init() error {
func Init(alerter *alerter.Alerter) error {
provider, err := NewTerraFormProvider()
if err != nil {
return err

View File

@ -3,6 +3,7 @@ package remote
import (
"fmt"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/remote/aws"
)
@ -19,10 +20,10 @@ func IsSupported(remote string) bool {
return false
}
func Activate(remote string) error {
func Activate(remote string, alerter *alerter.Alerter) error {
switch remote {
case aws.RemoteAWSTerraform:
return aws.Init()
return aws.Init(alerter)
default:
return fmt.Errorf("unsupported remote '%s'", remote)
}

View File

@ -6,18 +6,21 @@ import (
"github.com/sirupsen/logrus"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
)
type Scanner struct {
resourceSuppliers []resource.Supplier
runner *ParallelRunner
alerter *alerter.Alerter
}
func NewScanner(resourceSuppliers []resource.Supplier) *Scanner {
func NewScanner(resourceSuppliers []resource.Supplier, alerter *alerter.Alerter) *Scanner {
return &Scanner{
resourceSuppliers: resourceSuppliers,
runner: NewParallelRunner(context.TODO(), 10),
alerter: alerter,
}
}

View File

@ -5,7 +5,7 @@ import "fmt"
type FakeResource struct {
Id string
FooBar string
BarFoo string
BarFoo string `computed:"true"`
Json string `jsonstring:"true"`
Type string
Tags map[string]string
@ -17,6 +17,10 @@ type FakeResource struct {
Baz string `computed:"true"`
Bar string
}
StructSlice []struct {
String string `computed:"true"`
Array []string `computed:"true"`
}
}
func (d FakeResource) TerraformId() string {