package analyser import ( "encoding/json" "io/ioutil" "github.com/stretchr/testify/mock" "github.com/cloudskiff/driftctl/mocks" testresource "github.com/cloudskiff/driftctl/test/resource" "github.com/stretchr/testify/assert" "github.com/cloudskiff/driftctl/test/goldenfile" "github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/resource" "github.com/r3labs/diff/v2" "testing" ) func TestAnalyze(t *testing.T) { cases := []struct { name string iac []resource.Resource ignoredRes []resource.Resource cloud []resource.Resource ignoredDrift []struct { res resource.Resource path []string } alerts alerter.Alerts expected Analysis hasDrifted bool }{ { name: "TestNilValues", // Cover division by zero case iac: nil, cloud: nil, expected: Analysis{}, }, { name: "TestNothingToCompare", // Cover division by zero case iac: []resource.Resource{}, cloud: []resource.Resource{}, expected: Analysis{}, }, { name: "TestIgnoreFromCoverageIacNotInCloud", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, cloud: []resource.Resource{}, expected: Analysis{ summary: Summary{ TotalResources: 1, TotalDeleted: 1, }, deleted: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, }, hasDrifted: true, }, { name: "TestResourceIgnoredDeleted", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, ignoredRes: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, cloud: []resource.Resource{}, expected: Analysis{ summary: Summary{ TotalResources: 0, TotalDeleted: 0, }, }, hasDrifted: false, }, { name: "Test100PercentCoverage with ignore", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, &testresource.FakeResource{ Id: "foobar2", }, }, ignoredRes: []resource.Resource{ &testresource.FakeResource{ Id: "foobar2", }, }, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, &testresource.FakeResource{ Id: "foobar2", }, }, expected: Analysis{ managed: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, summary: Summary{ TotalManaged: 1, TotalResources: 1, }, }, }, { name: "Test100PercentCoverage", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, expected: Analysis{ managed: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, summary: Summary{ TotalManaged: 1, TotalResources: 1, }, }, }, { name: "TestUnmanagedResource", iac: []resource.Resource{}, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, expected: Analysis{ summary: Summary{ TotalResources: 1, TotalUnmanaged: 1, }, unmanaged: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", }, }, }, hasDrifted: true, }, { name: "TestDiff", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"baz", "bar"}, }, }, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "barfoo", BarFoo: "foobar", Struct: struct { Baz string `computed:"true"` Bar string }{"bar", "baz"}, }, }, expected: Analysis{ managed: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"baz", "bar"}, }, }, summary: Summary{ TotalResources: 1, TotalDrifted: 1, TotalManaged: 1, }, differences: []Difference{ { Res: &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"baz", "bar"}, }, Changelog: Changelog{ { Change: diff.Change{ Type: "update", From: "foobar", To: "barfoo", Path: []string{ "FooBar", }, }, }, { Change: diff.Change{ Type: "update", From: "barfoo", To: "foobar", Path: []string{ "BarFoo", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "baz", To: "bar", Path: []string{ "Struct", "Baz", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "bar", To: "baz", Path: []string{ "Struct", "Bar", }, }, }, }, }, }, alerts: alerter.Alerts{ "": { { Message: "You have diffs on computed fields, check the documentation for potential false positive drifts", }, }, }, }, hasDrifted: true, }, { name: "TestDiff with partial ignore", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "foobar", BarFoo: "barfoo", }, }, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "barfoo", BarFoo: "foobar", }, }, ignoredDrift: []struct { res resource.Resource path []string }{ { res: &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "foobar", BarFoo: "barfoo", }, path: []string{"FooBar"}, }, }, expected: Analysis{ managed: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "foobar", BarFoo: "barfoo", }, }, summary: Summary{ TotalResources: 1, TotalDrifted: 1, TotalManaged: 1, }, differences: []Difference{ { Res: &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "foobar", BarFoo: "barfoo", }, Changelog: Changelog{ { Change: diff.Change{ Type: "update", From: "barfoo", To: "foobar", Path: []string{ "BarFoo", }, }, Computed: true, }, }, }, }, alerts: alerter.Alerts{ "": { { Message: "You have diffs on computed fields, check the documentation for potential false positive drifts", }, }, }, }, hasDrifted: true, }, { name: "TestDiff with full ignore", iac: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", }, }, ignoredRes: []resource.Resource{ &testresource.FakeResource{ Id: "should_be_ignored", }, }, cloud: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "barfoo", BarFoo: "foobar", }, &testresource.FakeResource{ Id: "should_be_ignored", }, }, ignoredDrift: []struct { res resource.Resource path []string }{ { res: &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", }, path: []string{"FooBar"}, }, { res: &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", }, path: []string{"BarFoo"}, }, }, expected: Analysis{ managed: []resource.Resource{ &testresource.FakeResource{ Id: "foobar", FooBar: "foobar", BarFoo: "barfoo", }, }, summary: Summary{ TotalResources: 1, TotalDrifted: 0, TotalManaged: 1, }, }, 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: Changelog{ { Change: diff.Change{ Type: "update", From: "foobar", To: "barfoo", Path: []string{ "FooBar", }, }, }, { Change: diff.Change{ Type: "update", From: "barfoo", To: "foobar", Path: []string{ "BarFoo", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "baz", To: "bar", Path: []string{ "Struct", "Baz", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "bar", To: "baz", Path: []string{ "Struct", "Bar", }, }, }, { Change: diff.Change{ Type: "update", From: "foo", To: "oof", Path: []string{ "StructSlice", "0", "Array", "0", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "one", To: "two", Path: []string{ "StructSlice", "0", "String", }, }, Computed: true, }, }, }, }, 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: "You have diffs on computed fields, check the documentation for potential false positive drifts", }, }, }, }, hasDrifted: true, }, { name: "TestDiff with computed field send 1 alert", 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: "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: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"bazdiff", "bardiff"}, }, &testresource.FakeResource{ Id: "resource", Type: "other", FooBar: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"bazdiff", "bar"}, StructSlice: []struct { String string `computed:"true"` Array []string `computed:"true"` }{ {"onediff", []string{"foo", "diff"}}, }, }, }, alerts: alerter.Alerts{}, expected: Analysis{ managed: []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: "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: 2, TotalDrifted: 2, TotalManaged: 2, }, differences: []Difference{ { Res: &testresource.FakeResource{ Id: "foobar", Type: "fakeres", FooBar: "foobar", BarFoo: "barfoo", Struct: struct { Baz string `computed:"true"` Bar string }{"baz", "bar"}, }, Changelog: Changelog{ { Change: diff.Change{ Type: "update", From: "baz", To: "bazdiff", Path: []string{ "Struct", "Baz", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "bar", To: "bardiff", Path: []string{ "Struct", "Bar", }, }, Computed: false, }, }, }, { 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: Changelog{ { Change: diff.Change{ Type: "create", From: nil, To: "diff", Path: []string{ "StructSlice", "0", "Array", "1", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "baz", To: "bazdiff", Path: []string{ "Struct", "Baz", }, }, Computed: true, }, { Change: diff.Change{ Type: "update", From: "one", To: "onediff", Path: []string{ "StructSlice", "0", "String", }, }, Computed: true, }, }, }, }, alerts: alerter.Alerts{ "": { { Message: "You have diffs on computed fields, check the documentation for potential false positive drifts", }, }, }, }, hasDrifted: true, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { filter := &mocks.Filter{} for _, ignored := range c.ignoredRes { filter.On("IsResourceIgnored", ignored).Return(true) } filter.On("IsResourceIgnored", mock.Anything).Return(false) for _, s := range c.ignoredDrift { filter.On("IsFieldIgnored", s.res, s.path).Return(true) } filter.On("IsFieldIgnored", mock.Anything, mock.Anything).Return(false) al := alerter.NewAlerter() if c.alerts != nil { al.SetAlerts(c.alerts) } analyzer := NewAnalyzer(al) result, err := analyzer.Analyze(c.cloud, c.iac, filter) if err != nil { t.Error(err) return } if result.IsSync() == c.hasDrifted { t.Errorf("Drifted state does not match, got %t expected %t", result.IsSync(), !c.hasDrifted) } managedChanges, err := diff.Diff(result.Managed(), c.expected.Managed()) if err != nil { t.Fatalf("Unable to compare %+v", err) } if len(managedChanges) > 0 { for _, change := range managedChanges { t.Errorf("%+v", change) } } unmanagedChanges, err := diff.Diff(result.Unmanaged(), c.expected.Unmanaged()) if err != nil { t.Fatalf("Unable to compare %+v", err) } if len(unmanagedChanges) > 0 { for _, change := range unmanagedChanges { t.Errorf("%+v", change) } } deletedChanges, err := diff.Diff(result.Deleted(), c.expected.Deleted()) if err != nil { t.Fatalf("Unable to compare %+v", err) } if len(deletedChanges) > 0 { for _, change := range deletedChanges { t.Errorf("%+v", change) } } diffChanges, err := diff.Diff(result.Differences(), c.expected.Differences()) if err != nil { t.Fatalf("Unable to compare %+v", err) } if len(diffChanges) > 0 { for _, change := range diffChanges { t.Errorf("%+v", change) } } summaryChanges, err := diff.Diff(c.expected.Summary(), result.Summary()) if err != nil { t.Fatalf("Unable to compare %+v", err) } if len(summaryChanges) > 0 { for _, change := range summaryChanges { t.Errorf("%+v", change) } } 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) } } }) } } func TestAnalysis_MarshalJSON(t *testing.T) { goldenFile := "./testdata/output.json" analysis := Analysis{} analysis.AddManaged( &testresource.FakeResource{ Id: "AKIA5QYBVVD25KFXJHYJ", Type: "aws_iam_access_key", }, &testresource.FakeResource{ Id: "driftctl2", Type: "aws_managed_resource", }, ) analysis.AddUnmanaged( &testresource.FakeResource{ Id: "driftctl", Type: "aws_s3_bucket_policy", }, &testresource.FakeResource{ Id: "driftctl", Type: "aws_s3_bucket_notification", }, ) analysis.AddDeleted( &testresource.FakeResource{ Id: "test-driftctl2", Type: "aws_iam_user", FooBar: "test", }, &testresource.FakeResource{ Id: "AKIA5QYBVVD2Y6PBAAPY", Type: "aws_iam_access_key", }, ) analysis.AddDifference(Difference{ Res: &testresource.FakeResource{ Id: "AKIA5QYBVVD25KFXJHYJ", Type: "aws_iam_access_key", }, Changelog: []Change{ { Change: diff.Change{ Type: "update", Path: []string{"Status"}, From: "Active", To: "Inactive", }, }, }, }) analysis.SetAlerts(alerter.Alerts{ "aws_iam_access_key": { { Message: "This is an alert", }, }, }) got, err := json.MarshalIndent(analysis, "", "\t") if err != nil { t.Fatal(err) } if *goldenfile.Update == "TestAnalysis_MarshalJSON" { if err := ioutil.WriteFile(goldenFile, got, 0600); err != nil { t.Fatal(err) } } expected, err := ioutil.ReadFile(goldenFile) if err != nil { t.Fatal(err) } assert.Nil(t, err) assert.Equal(t, string(expected), string(got)) } func TestAnalysis_UnmarshalJSON(t *testing.T) { expected := Analysis{ summary: Summary{ TotalResources: 6, TotalDrifted: 1, TotalUnmanaged: 2, TotalDeleted: 2, TotalManaged: 2, }, managed: []resource.Resource{ resource.SerializedResource{ Id: "AKIA5QYBVVD25KFXJHYJ", Type: "aws_iam_access_key", }, resource.SerializedResource{ Id: "test-managed", Type: "aws_iam_user", }, }, unmanaged: []resource.Resource{ resource.SerializedResource{ Id: "driftctl", Type: "aws_s3_bucket_policy", }, resource.SerializedResource{ Id: "driftctl", Type: "aws_s3_bucket_notification", }, }, deleted: []resource.Resource{ resource.SerializedResource{ Id: "test-driftctl2", Type: "aws_iam_user", }, resource.SerializedResource{ Id: "AKIA5QYBVVD2Y6PBAAPY", Type: "aws_iam_access_key", }, }, differences: []Difference{ { Res: resource.SerializedResource{ Id: "AKIA5QYBVVD25KFXJHYJ", Type: "aws_iam_access_key", }, Changelog: []Change{ { Change: diff.Change{ Type: "update", Path: []string{"Status"}, From: "Active", To: "Inactive", }, }, }, }, }, alerts: alerter.Alerts{ "aws_iam_access_key": { { Message: "This is an alert", }, }, }, } got := Analysis{} input, err := ioutil.ReadFile("./testdata/input.json") if err != nil { t.Fatal(err) } err = json.Unmarshal(input, &got) if err != nil { t.Fatal(err) } assert.Equal(t, expected, got) assert.Equal(t, 33, got.Coverage()) assert.Equal(t, 2, got.Summary().TotalUnmanaged) assert.Equal(t, 2, got.Summary().TotalManaged) assert.Equal(t, 2, got.Summary().TotalDeleted) assert.Equal(t, 6, got.Summary().TotalResources) assert.Equal(t, 1, got.Summary().TotalDrifted) }