From c7eccd5b7ccb4345ec61d452786a66c07eeb7409 Mon Sep 17 00:00:00 2001 From: Martin Guibert Date: Tue, 29 Jun 2021 11:59:16 +0200 Subject: [PATCH] add expander for eip association --- pkg/driftctl.go | 1 + pkg/driftctl_test.go | 1695 +++++++++++++++++ .../aws_eip_association_expander.go | 70 + pkg/resource/aws/aws_eip_association_test.go | 30 + .../aws_eip_association/.terraform.lock.hcl | 21 + .../testdata/acc/aws_eip_association/mainf.tf | 61 + 6 files changed, 1878 insertions(+) create mode 100644 pkg/driftctl_test.go create mode 100644 pkg/middlewares/aws_eip_association_expander.go create mode 100644 pkg/resource/aws/aws_eip_association_test.go create mode 100644 pkg/resource/aws/testdata/acc/aws_eip_association/.terraform.lock.hcl create mode 100644 pkg/resource/aws/testdata/acc/aws_eip_association/mainf.tf diff --git a/pkg/driftctl.go b/pkg/driftctl.go index 51ed61d8..c1186a90 100644 --- a/pkg/driftctl.go +++ b/pkg/driftctl.go @@ -100,6 +100,7 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) { middlewares.NewAwsSNSTopicPolicyExpander(d.resourceFactory, d.resourceSchemaRepository), middlewares.NewAwsRoleManagedPolicyExpander(d.resourceFactory), middlewares.NewTagsAllManager(), + middlewares.NewEipAssociationExpander(d.resourceFactory), ) if !d.opts.StrictMode { diff --git a/pkg/driftctl_test.go b/pkg/driftctl_test.go new file mode 100644 index 00000000..a132dd0e --- /dev/null +++ b/pkg/driftctl_test.go @@ -0,0 +1,1695 @@ +package pkg_test + +import ( + "reflect" + "testing" + + "github.com/cloudskiff/driftctl/pkg" + "github.com/cloudskiff/driftctl/pkg/alerter" + "github.com/cloudskiff/driftctl/pkg/analyser" + "github.com/cloudskiff/driftctl/pkg/filter" + "github.com/cloudskiff/driftctl/pkg/output" + "github.com/cloudskiff/driftctl/pkg/resource" + "github.com/cloudskiff/driftctl/pkg/resource/aws" + "github.com/cloudskiff/driftctl/pkg/resource/github" + "github.com/cloudskiff/driftctl/pkg/terraform" + "github.com/cloudskiff/driftctl/test" + testresource "github.com/cloudskiff/driftctl/test/resource" + "github.com/r3labs/diff/v2" + "github.com/stretchr/testify/mock" +) + +type TestProvider struct { + Name string + Version string +} + +type TestCase struct { + name string + provider *TestProvider + stateResources []resource.Resource + remoteResources []resource.Resource + mocks func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) + assert func(result *test.ScanResult, err error) + options *pkg.ScanOptions +} + +type TestCases []TestCase + +func runTest(t *testing.T, cases TestCases) { + for _, c := range cases { + if c.provider == nil { + c.provider = &TestProvider{ + Name: "aws", + Version: "3.19.0", + } + } + repo := testresource.InitFakeSchemaRepository(c.provider.Name, c.provider.Version) + aws.InitResourcesMetadata(repo) + github.InitResourcesMetadata(repo) + t.Run(c.name, func(t *testing.T) { + testAlerter := alerter.NewAlerter() + + if c.stateResources == nil { + c.stateResources = []resource.Resource{} + } + + for _, res := range c.stateResources { + abstractResource, ok := res.(*resource.AbstractResource) + if ok { + schema, _ := repo.GetSchema(abstractResource.TerraformType()) + abstractResource.Sch = schema + } + } + + stateSupplier := &resource.MockSupplier{} + stateSupplier.On("Resources").Return(c.stateResources, nil) + + if c.remoteResources == nil { + c.remoteResources = []resource.Resource{} + } + + for _, res := range c.remoteResources { + abstractResource, ok := res.(*resource.AbstractResource) + if ok { + schema, _ := repo.GetSchema(abstractResource.TerraformType()) + abstractResource.Sch = schema + } + } + remoteSupplier := &resource.MockSupplier{} + remoteSupplier.On("Resources").Return(c.remoteResources, nil) + + var resourceFactory resource.ResourceFactory = terraform.NewTerraformResourceFactory(repo) + + if c.mocks != nil { + resourceFactory = &terraform.MockResourceFactory{} + c.mocks(resourceFactory, repo) + } + + if c.options == nil { + c.options = &pkg.ScanOptions{Deep: true} + } + + scanProgress := &output.MockProgress{} + scanProgress.On("Start").Return().Once() + scanProgress.On("Stop").Return().Once() + + iacProgress := &output.MockProgress{} + iacProgress.On("Start").Return().Once() + iacProgress.On("Stop").Return().Once() + + driftctl := pkg.NewDriftCTL(remoteSupplier, stateSupplier, testAlerter, resourceFactory, c.options, scanProgress, iacProgress, repo) + + analysis, err := driftctl.Run() + + c.assert(test.NewScanResult(t, analysis), err) + scanProgress.AssertExpectations(t) + }) + } +} + +func matchByAttributes(input, attrs map[string]interface{}) bool { + for k, v := range attrs { + if value, ok := input[k]; !ok || !reflect.DeepEqual(value, v) { + return false + } + } + return true +} + +func TestDriftctlRun_BasicBehavior(t *testing.T) { + + cases := TestCases{ + { + name: "analysis duration is set", + stateResources: []resource.Resource{}, + remoteResources: []resource.Resource{}, + assert: func(result *test.ScanResult, err error) { + result.NotZero(result.Duration) + }, + }, + { + name: "infrastructure should be in sync", + stateResources: []resource.Resource{ + &testresource.FakeResource{}, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{}, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertInfrastructureIsInSync() + }, + options: func(t *testing.T) *pkg.ScanOptions { + return &pkg.ScanOptions{Deep: true} + }(t), + }, + { + name: "we should have deleted resource", + stateResources: []resource.Resource{ + &testresource.FakeResource{}, + }, + remoteResources: []resource.Resource{}, + assert: func(result *test.ScanResult, err error) { + result.AssertDeletedCount(1) + }, + }, + { + name: "we should have unmanaged resource", + stateResources: []resource.Resource{}, + remoteResources: []resource.Resource{ + &testresource.FakeResource{}, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertUnmanagedCount(1) + }, + }, + { + name: "we should have changes of field update", + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "foobar": "barfoo", + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "foobar": "foobar", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("fake", "FakeResource", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"foobar"}, + From: "barfoo", + To: "foobar", + }, + Computed: false, + }) + }, + }, + { + name: "we should have changes on computed field", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "fake", + Type: aws.AwsAmiResourceType, + Attrs: &resource.Attributes{ + "arn": "barfoo", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "fake", + Type: aws.AwsAmiResourceType, + Attrs: &resource.Attributes{ + "arn": "foobar", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("fake", aws.AwsAmiResourceType, analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"arn"}, + From: "barfoo", + To: "foobar", + }, + Computed: true, + }) + }, + }, + { + name: "we should have changes on deleted field", + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "tags": map[string]string{ + "tag1": "deleted", + }, + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "tags": map[string]string{}, + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("fake", "FakeResource", analyser.Change{ + Change: diff.Change{ + Type: diff.DELETE, + Path: []string{"tags", "tag1"}, + From: "deleted", + To: nil, + }, + Computed: false, + }) + }, + }, + { + name: "we should have changes of added field", + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "tags": map[string]string{}, + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{ + "tags": map[string]string{ + "tag1": "added", + }, + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("fake", "FakeResource", analyser.Change{ + Change: diff.Change{ + Type: diff.CREATE, + Path: []string{"tags", "tag1"}, + From: nil, + To: "added", + }, + Computed: false, + }) + }, + }, + { + name: "we should ignore default AWS IAM role when strict mode is disabled", + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On( + "CreateAbstractResource", + aws.AwsIamPolicyAttachmentResourceType, + "role-test-1-policy-test-1", + map[string]interface{}{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + ).Once().Return(&resource.AbstractResource{ + Id: "role-test-1-policy-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + }) + }, + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "role-policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "role-test-1", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/aws-service-role/test", + }, + }, + &resource.AbstractResource{ + Id: "role-policy-test-1", + Type: aws.AwsIamRolePolicyResourceType, + Attrs: &resource.Attributes{ + "role": "role-test-1", + }, + }, + &resource.AbstractResource{ + Id: "role-policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + &resource.AbstractResource{ + Id: "policy-attachment-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "users": []interface{}{}, + "roles": []interface{}{"role-test-1"}, + }, + }, + &resource.AbstractResource{ + Id: "role-test-2", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/not-aws-service-role/test", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(2) + result.AssertUnmanagedCount(2) + result.AssertDeletedCount(0) + result.AssertDriftCountTotal(0) + }, + options: func(t *testing.T) *pkg.ScanOptions { + return &pkg.ScanOptions{ + StrictMode: false, + Deep: true, + } + }(t), + }, + { + name: "we should not ignore default AWS IAM role when strict mode is enabled", + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On( + "CreateAbstractResource", + aws.AwsIamPolicyAttachmentResourceType, + "role-test-1-policy-test-1", + map[string]interface{}{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + ).Once().Return(&resource.AbstractResource{ + Id: "role-test-1-policy-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + }) + }, + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "role-test-1", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/aws-service-role/test", + }, + }, + &resource.AbstractResource{ + Id: "role-policy-test-1", + Type: aws.AwsIamRolePolicyResourceType, + Attrs: &resource.Attributes{ + "role": "role-test-1", + }, + }, + &resource.AbstractResource{ + Id: "policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + &resource.AbstractResource{ + Id: "policy-attachment-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "users": []interface{}{}, + "roles": []interface{}{"role-test-1"}, + }, + }, + &resource.AbstractResource{ + Id: "role-test-2", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/not-aws-service-role/test", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(2) + result.AssertUnmanagedCount(4) + result.AssertDeletedCount(0) + result.AssertDriftCountTotal(0) + }, + options: func(t *testing.T) *pkg.ScanOptions { + return &pkg.ScanOptions{ + StrictMode: true, + Deep: true, + } + }(t), + }, + { + name: "we should not ignore default AWS IAM role when strict mode is enabled and a filter is specified", + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On( + "CreateAbstractResource", + aws.AwsIamPolicyAttachmentResourceType, + "role-test-1-policy-test-1", + map[string]interface{}{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + ).Once().Return(&resource.AbstractResource{ + Id: "role-test-1-policy-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "roles": []interface{}{"role-test-1"}, + }, + }) + }, + stateResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + }, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "fake", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "role-test-1", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/aws-service-role/test", + }, + }, + &resource.AbstractResource{ + Id: "role-policy-test-1", + Type: aws.AwsIamRolePolicyResourceType, + Attrs: &resource.Attributes{ + "role": "role-test-1", + }, + }, + &resource.AbstractResource{ + Id: "policy-test-1", + Type: aws.AwsIamPolicyResourceType, + Attrs: &resource.Attributes{ + "arn": "policy-test-1", + }, + }, + &resource.AbstractResource{ + Id: "policy-attachment-test-1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy-test-1", + "users": []interface{}{}, + "roles": []interface{}{"role-test-1"}, + }, + }, + &resource.AbstractResource{ + Id: "role-test-2", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "path": "/not-aws-service-role/test", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertCoverage(0) + result.AssertInfrastructureIsNotSync() + result.AssertManagedCount(0) + result.AssertUnmanagedCount(1) + result.AssertDeletedCount(0) + result.AssertDriftCountTotal(0) + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Id=='role-test-1'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{ + Filter: f, + StrictMode: true, + Deep: true, + } + }(t), + }, + } + + runTest(t, cases) +} + +func TestDriftctlRun_BasicFilter(t *testing.T) { + cases := TestCases{ + { + name: "test filtering on Type", + stateResources: []resource.Resource{}, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "res1", + Type: "not-filtered", + Attrs: &resource.Attributes{}, + }, + &testresource.FakeResource{ + Id: "res2", + Type: "filtered", + Attrs: &resource.Attributes{}, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertUnmanagedCount(1) + result.AssertResourceUnmanaged("res2", "filtered") + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='filtered'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test filtering on Id", + stateResources: []resource.Resource{}, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "res1", + Type: "not-filtered", + Attrs: &resource.Attributes{}, + }, + &testresource.FakeResource{ + Id: "res2", + Type: "filtered", + Attrs: &resource.Attributes{}, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertUnmanagedCount(1) + result.AssertResourceUnmanaged("res2", "filtered") + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Id=='res2'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test filtering on attribute", + stateResources: []resource.Resource{}, + remoteResources: []resource.Resource{ + &testresource.FakeResource{ + Id: "res1", + Type: "filtered", + Attrs: &resource.Attributes{ + "test_field": "value to filter on", + }, + }, + &testresource.FakeResource{ + Id: "res2", + Type: "not-filtered", + Attrs: &resource.Attributes{}, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertUnmanagedCount(1) + result.AssertResourceUnmanaged("res1", "filtered") + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Attr.test_field=='value to filter on'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + } + + runTest(t, cases) +} + +func TestDriftctlRun_Middlewares(t *testing.T) { + cases := TestCases{ + { + name: "test bucket policy expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsS3BucketResourceType, + Attrs: &resource.Attributes{ + "bucket": "foo", + "policy": "{\"Id\":\"foo\"}", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsS3BucketPolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "bucket": "foo", + "policy": "{\"Id\":\"bar\"}", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On( + "CreateAbstractResource", + aws.AwsS3BucketPolicyResourceType, + "foo", + map[string]interface{}{ + "id": "foo", + "bucket": "foo", + "policy": "{\"Id\":\"foo\"}", + }, + ).Once().Return(&resource.AbstractResource{ + Id: "foo", + Type: aws.AwsS3BucketPolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "bucket": "foo", + "policy": "{\"Id\":\"foo\"}", + }, + Sch: getSchema(repo, aws.AwsS3BucketPolicyResourceType), + }) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("foo", "aws_s3_bucket_policy", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"policy"}, + From: "{\"Id\":\"foo\"}", + To: "{\"Id\":\"bar\"}", + }, + Computed: false, + JsonString: true, + }) + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_s3_bucket_policy' && Attr.bucket=='foo'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test instance block device middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "dummy-instance", + Type: "aws_instance", + Attrs: &resource.Attributes{ + "availability_zone": "us-east-1", + "root_block_device": []interface{}{ + map[string]interface{}{ + "volume_id": "vol-02862d9b39045a3a4", + "volume_type": "gp2", + }, + }, + "ebs_block_device": []interface{}{ + map[string]interface{}{ + "volume_id": "vol-018c5ae89895aca4c", + "encrypted": true, + }, + }, + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "vol-018c5ae89895aca4c", + Type: "aws_ebs_volume", + Attrs: &resource.Attributes{ + "encrypted": false, + "multi_attach_enabled": false, + "availability_zone": "us-east-1", + }, + }, + &resource.AbstractResource{ + Id: "vol-02862d9b39045a3a4", + Type: "aws_ebs_volume", + Attrs: &resource.Attributes{ + "type": "gp3", + "multi_attach_enabled": false, + "availability_zone": "us-east-1", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + foo := resource.AbstractResource{ + Id: "vol-018c5ae89895aca4c", + Type: "aws_ebs_volume", + Attrs: &resource.Attributes{ + "encrypted": true, + "multi_attach_enabled": false, + "availability_zone": "us-east-1", + }, + Sch: getSchema(repo, "aws_ebs_volume"), + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_ebs_volume", mock.Anything, mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "vol-018c5ae89895aca4c", + "availability_zone": "us-east-1", + "encrypted": true, + "multi_attach_enabled": false, + }) + })).Times(1).Return(&foo, nil) + + bar := resource.AbstractResource{ + Id: "vol-02862d9b39045a3a4", + Type: "aws_ebs_volume", + Attrs: &resource.Attributes{ + "type": "gp2", + "multi_attach_enabled": false, + "availability_zone": "us-east-1", + }, + Sch: getSchema(repo, "aws_ebs_volume"), + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_ebs_volume", mock.Anything, mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "vol-02862d9b39045a3a4", + "availability_zone": "us-east-1", + "type": "gp2", + "multi_attach_enabled": false, + }) + })).Times(1).Return(&bar, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(2) + result.AssertResourceHasDrift("vol-02862d9b39045a3a4", "aws_ebs_volume", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"type"}, + From: "gp2", + To: "gp3", + }, + Computed: true, + }) + result.AssertResourceHasDrift("vol-018c5ae89895aca4c", "aws_ebs_volume", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"encrypted"}, + From: true, + To: false, + }, + Computed: true, + }) + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_ebs_volume' && Attr.availability_zone=='us-east-1'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test route table expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "table", + Type: "aws_route_table", + Attrs: &resource.Attributes{ + "route": []interface{}{ + map[string]interface{}{ + "gateway_id": "igw-07b7844a8fd17a638", + "cidr_block": "0.0.0.0/0", + }, + map[string]interface{}{ + "gateway_id": "igw-07b7844a8fd17a638", + "cidr_block": "", + "ipv6_cidr_block": "::/0", + }, + }, + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "r-table1080289494", + Type: aws.AwsRouteResourceType, + Attrs: &resource.Attributes{ + "route_table_id": "table", + "origin": "CreateRoute", + "destination_cidr_block": "0.0.0.0/0", + "gateway_id": "igw-07b7844a8fd17a638", + "state": "active", + }, + }, + &resource.AbstractResource{ + Id: "r-table2750132062", + Type: aws.AwsRouteResourceType, + Attrs: &resource.Attributes{ + "route_table_id": "table", + "origin": "CreateRoute", + "destination_ipv6_cidr_block": "::/0", + "gateway_id": "igw-07b7844a8fd17a638", + "state": "active", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_route", "r-table1080289494", mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "destination_cidr_block": "0.0.0.0/0", + "gateway_id": "igw-07b7844a8fd17a638", + "origin": "CreateRoute", + "route_table_id": "table", + "state": "active", + }) + })).Times(1).Return(&resource.AbstractResource{ + Id: "r-table1080289494", + Type: aws.AwsRouteResourceType, + Attrs: &resource.Attributes{ + "route_table_id": "table", + "origin": "CreateRoute", + "destination_cidr_block": "0.0.0.0/0", + "gateway_id": "igw-07b7844a8fd17a638", + "state": "active", + }, + }, nil) + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_route", "r-table2750132062", mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "destination_ipv6_cidr_block": "::/0", + "gateway_id": "igw-07b7844a8fd17a638", + "origin": "CreateRoute", + "route_table_id": "table", + "state": "active", + }) + })).Times(1).Return(&resource.AbstractResource{ + Id: "r-table2750132062", + Type: aws.AwsRouteResourceType, + Attrs: &resource.Attributes{ + "route_table_id": "table", + "origin": "CreateRoute", + "destination_ipv6_cidr_block": "::/0", + "gateway_id": "igw-07b7844a8fd17a638", + "state": "active", + }, + }, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(2) + result.AssertInfrastructureIsInSync() + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_route' && Attr.gateway_id=='igw-07b7844a8fd17a638'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test sns topic policy expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSnsTopicResourceType, + Attrs: &resource.Attributes{ + "arn": "arn", + "id": "foo", + "policy": "{\"policy\":\"bar\"}", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSnsTopicPolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "arn": "arn", + "policy": "{\"policy\":\"baz\"}", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_sns_topic_policy", "foo", map[string]interface{}{ + "id": "foo", + "arn": "arn", + "policy": "{\"policy\":\"bar\"}", + }).Times(1).Return(&resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSnsTopicPolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "arn": "arn", + "policy": "{\"policy\":\"bar\"}", + }, + Sch: getSchema(repo, aws.AwsSnsTopicPolicyResourceType), + }, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("foo", "aws_sns_topic_policy", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"policy"}, + From: "{\"policy\":\"bar\"}", + To: "{\"policy\":\"baz\"}", + }, + Computed: false, + JsonString: true, + }) + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_sns_topic_policy' && Attr.arn=='arn'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test sqs queue policy expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSqsQueueResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "policy": "{\"policy\":\"bar\"}", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSqsQueuePolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "queue_url": "foo", + "policy": "{\"policy\":\"baz\"}", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_sqs_queue_policy", "foo", map[string]interface{}{ + "id": "foo", + "queue_url": "foo", + "policy": "{\"policy\":\"bar\"}", + }).Times(1).Return(&resource.AbstractResource{ + Id: "foo", + Type: aws.AwsSqsQueuePolicyResourceType, + Attrs: &resource.Attributes{ + "id": "foo", + "queue_url": "foo", + "policy": "{\"policy\":\"bar\"}", + }, + Sch: getSchema(repo, aws.AwsSqsQueuePolicyResourceType), + }, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(1) + result.AssertResourceHasDrift("foo", "aws_sqs_queue_policy", analyser.Change{ + Change: diff.Change{ + Type: diff.UPDATE, + Path: []string{"policy"}, + From: "{\"policy\":\"bar\"}", + To: "{\"policy\":\"baz\"}", + }, + Computed: false, + JsonString: true, + }) + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_sqs_queue_policy' && Attr.queue_url=='foo'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test security group rule sanitizer middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3970541193", + Attrs: &resource.Attributes{ + "id": "sgrule-3970541193", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "self": true, + "source_security_group_id": "sg-0254c038e32f25530", + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-845917806", + Attrs: &resource.Attributes{ + "id": "sgrule-845917806", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"0.0.0.0/0"}, + "ipv6_cidr_blocks": []interface{}{"::/0"}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-294318973", + Attrs: &resource.Attributes{ + "id": "sgrule-294318973", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"1.2.0.0/16", "5.6.7.0/24"}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2471889226", + Attrs: &resource.Attributes{ + "id": "sgrule-2471889226", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(0), + "prefix_list_ids": []interface{}{"pl-abb451c2"}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3587309474", + Attrs: &resource.Attributes{ + "id": "sgrule-3587309474", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "source_security_group_id": "sg-9e0204ff", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3970541193", + Attrs: &resource.Attributes{ + "id": "sgrule-3970541193", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "self": true, + "source_security_group_id": "sg-0254c038e32f25530", + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-1707973622", + Attrs: &resource.Attributes{ + "id": "sgrule-1707973622", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"0.0.0.0/0"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2821752134", + Attrs: &resource.Attributes{ + "id": "sgrule-2821752134", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{}, + "ipv6_cidr_blocks": []interface{}{"::/0"}, + "prefix_list_ids": []interface{}{}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2165103420", + Attrs: &resource.Attributes{ + "id": "sgrule-2165103420", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"5.6.7.0/24"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2582518759", + Attrs: &resource.Attributes{ + "id": "sgrule-2582518759", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"1.2.0.0/16"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2471889226", + Attrs: &resource.Attributes{ + "id": "sgrule-2471889226", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(0), + "prefix_list_ids": []interface{}{"pl-abb451c2"}, + }, + }, + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3587309474", + Attrs: &resource.Attributes{ + "id": "sgrule-3587309474", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "source_security_group_id": "sg-9e0204ff", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + rule1 := resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-1707973622", + Attrs: &resource.Attributes{ + "id": "sgrule-1707973622", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{ + "0.0.0.0/0", + }, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_security_group_rule", rule1.Id, + mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "sgrule-1707973622", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"0.0.0.0/0"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }) + })).Times(1).Return(&rule1, nil) + + rule2 := resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2821752134", + Attrs: &resource.Attributes{ + "id": "sgrule-2821752134", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{}, + "ipv6_cidr_blocks": []interface{}{ + "::/0", + }, + "prefix_list_ids": []interface{}{}, + }, + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_security_group_rule", rule2.Id, + mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "sgrule-2821752134", + "type": "egress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{}, + "ipv6_cidr_blocks": []interface{}{"::/0"}, + "prefix_list_ids": []interface{}{}, + }) + })).Times(1).Return(&rule2, nil) + + rule3 := resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2165103420", + Attrs: &resource.Attributes{ + "id": "sgrule-2165103420", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{ + "5.6.7.0/24", + }, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_security_group_rule", rule3.Id, + mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "sgrule-2165103420", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"5.6.7.0/24"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }) + })).Times(1).Return(&rule3, nil) + + rule4 := resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-2582518759", + Attrs: &resource.Attributes{ + "id": "sgrule-2582518759", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{ + "1.2.0.0/16", + }, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }, + } + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", "aws_security_group_rule", rule4.Id, + mock.MatchedBy(func(input map[string]interface{}) bool { + return matchByAttributes(input, map[string]interface{}{ + "id": "sgrule-2582518759", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "-1", + "from_port": float64(0), + "to_port": float64(0), + "cidr_blocks": []interface{}{"1.2.0.0/16"}, + "ipv6_cidr_blocks": []interface{}{}, + "prefix_list_ids": []interface{}{}, + }) + })).Times(1).Return(&rule4, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(7) + result.AssertInfrastructureIsInSync() + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_security_group_rule' && Attr.security_group_id=='sg-0254c038e32f25530'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test iam_policy_attachment_transformer & iam_policy_attachment_expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3970541193", + Attrs: &resource.Attributes{ + "id": "sgrule-3970541193", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "self": true, + "source_security_group_id": "sg-0254c038e32f25530", + }, + }, + &resource.AbstractResource{ + Id: "iduser1", + Type: aws.AwsIamUserPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "user": "user1", + }, + }, + &resource.AbstractResource{ + Id: "idrole1", + Type: aws.AwsIamRolePolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "role": "role1", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Type: aws.AwsSecurityGroupRuleResourceType, + Id: "sgrule-3970541193", + Attrs: &resource.Attributes{ + "id": "sgrule-3970541193", + "type": "ingress", + "security_group_id": "sg-0254c038e32f25530", + "protocol": "tcp", + "from_port": float64(0), + "to_port": float64(65535), + "self": true, + "source_security_group_id": "sg-0254c038e32f25530", + }, + }, + &resource.AbstractResource{ + Id: "iduser1", + Type: aws.AwsIamUserPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "user": "user1", + }, + }, + &resource.AbstractResource{ + Id: "idrole1", + Type: aws.AwsIamRolePolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "role": "role1", + }, + }, + }, + mocks: func(factory resource.ResourceFactory, repo resource.SchemaRepositoryInterface) { + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", aws.AwsIamPolicyAttachmentResourceType, "iduser1", map[string]interface{}{ + "id": "iduser1", + "policy_arn": "policy_arn1", + "users": []interface{}{"user1"}, + "groups": []interface{}{}, + "roles": []interface{}{}, + }).Twice().Return(&resource.AbstractResource{ + Id: "id1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "id": "iduser1", + "policy_arn": "policy_arn1", + "users": []interface{}{"user1"}, + "groups": []interface{}{}, + "roles": []interface{}{}, + }, + }, nil) + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", aws.AwsIamPolicyAttachmentResourceType, "user1-policy_arn1", map[string]interface{}{ + "policy_arn": "policy_arn1", + "users": []interface{}{"user1"}, + }).Twice().Return(&resource.AbstractResource{ + Id: "user1-policy_arn1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "users": []interface{}{"user1"}, + }, + }, nil) + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", aws.AwsIamPolicyAttachmentResourceType, "idrole1", map[string]interface{}{ + "id": "idrole1", + "policy_arn": "policy_arn1", + "users": []interface{}{}, + "groups": []interface{}{}, + "roles": []interface{}{"role1"}, + }).Twice().Return(&resource.AbstractResource{ + Id: "idrole1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "id": "idrole1", + "policy_arn": "policy_arn1", + "users": []interface{}{}, + "groups": []interface{}{}, + "roles": []interface{}{"role1"}, + }, + }, nil) + factory.(*terraform.MockResourceFactory).On("CreateAbstractResource", aws.AwsIamPolicyAttachmentResourceType, "role1-policy_arn1", map[string]interface{}{ + "policy_arn": "policy_arn1", + "roles": []interface{}{"role1"}, + }).Twice().Return(&resource.AbstractResource{ + Id: "role1-policy_arn1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "policy_arn1", + "roles": []interface{}{"role1"}, + }, + }, nil) + }, + assert: func(result *test.ScanResult, err error) { + result.AssertManagedCount(2) + result.AssertInfrastructureIsInSync() + }, + options: func(t *testing.T) *pkg.ScanOptions { + filterStr := "Type=='aws_iam_policy_attachment'" + f, err := filter.BuildExpression(filterStr) + if err != nil { + t.Fatalf("Unable to build filter expression: %s\n%s", filterStr, err) + } + + return &pkg.ScanOptions{Filter: f, Deep: true} + }(t), + }, + { + name: "test aws role managed policy expander", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "role_with_managed_policy_attr", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "name": "role_with_managed_policy_attr", + "managed_policy_arns": []interface{}{ + "arn1", + "arn2", + }, + }, + }, + &resource.AbstractResource{ + Id: "role_with_managed_policy_attr-arn2", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "arn2", + "roles": []interface{}{"role_with_managed_policy_attr"}, + }, + }, + &resource.AbstractResource{ + Id: "role_with_empty_managed_policy_attribute", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "managed_policy_arns": []interface{}{}, + }, + }, + &resource.AbstractResource{ + Id: "role_without_managed_policy_attribute", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{}, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "role_with_managed_policy_attr", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{ + "name": "role_with_managed_policy_attr", + }, + }, + &resource.AbstractResource{ + Id: "role_with_managed_policy_attr-arn1", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "arn1", + "roles": []interface{}{"role_with_managed_policy_attr"}, + }, + }, + &resource.AbstractResource{ + Id: "role_with_managed_policy_attr-arn2", + Type: aws.AwsIamPolicyAttachmentResourceType, + Attrs: &resource.Attributes{ + "policy_arn": "arn2", + "roles": []interface{}{"role_with_managed_policy_attr"}, + }, + }, + &resource.AbstractResource{ + Id: "role_with_empty_managed_policy_attribute", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "role_without_managed_policy_attribute", + Type: aws.AwsIamRoleResourceType, + Attrs: &resource.Attributes{}, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertInfrastructureIsInSync() + result.AssertManagedCount(5) + }, + }, + { + name: "test aws eip association expander middleware", + stateResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "ID", + Type: "ANOTHERTYPE", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "associdpresentinstate", + Type: aws.AwsEipAssociationResourceType, + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "associdpresentinstate", + Type: aws.AwsEipResourceType, + Attrs: &resource.Attributes{ + "association_id": "associdpresentinstate", + }, + }, + &resource.AbstractResource{ + Id: "associdNOTpresentinstate", + Type: aws.AwsEipResourceType, + Attrs: &resource.Attributes{ + "association_id": "associdNOTpresentinstate", + "instance": "instanceidNOTpresentinstate", + "network_interface": "networkinterface", + "private_ip": "privateip", + "public_ip": "publicip", + }, + }, + }, + remoteResources: []resource.Resource{ + &resource.AbstractResource{ + Id: "ID", + Type: "ANOTHERTYPE", + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "associdpresentinstate", + Type: aws.AwsEipAssociationResourceType, + Attrs: &resource.Attributes{}, + }, + &resource.AbstractResource{ + Id: "associdpresentinstate", + Type: aws.AwsEipResourceType, + Attrs: &resource.Attributes{ + "association_id": "associdpresentinstate", + }, + }, + &resource.AbstractResource{ + Id: "associdNOTpresentinstate", + Type: aws.AwsEipAssociationResourceType, + Attrs: &resource.Attributes{ + "allocation_id": "associdNOTpresentinstate", + "id": "associdNOTpresentinstate", + "instance_id": "instanceidNOTpresentinstate", + "network_interface_id": "networkinterface", + "private_ip_address": "privateip", + "public_ip": "publicip", + }, + }, + &resource.AbstractResource{ + Id: "associdNOTpresentinstate", + Type: aws.AwsEipResourceType, + Attrs: &resource.Attributes{ + "association_id": "associdNOTpresentinstate", + "instance": "instanceidNOTpresentinstate", + "network_interface": "networkinterface", + "private_ip": "privateip", + "public_ip": "publicip", + }, + }, + }, + assert: func(result *test.ScanResult, err error) { + result.AssertInfrastructureIsInSync() + result.AssertManagedCount(5) + }, + }, + } + + runTest(t, cases) +} + +func getSchema(repo resource.SchemaRepositoryInterface, resourceType string) *resource.Schema { + sch, _ := repo.GetSchema(resourceType) + return sch +} diff --git a/pkg/middlewares/aws_eip_association_expander.go b/pkg/middlewares/aws_eip_association_expander.go new file mode 100644 index 00000000..2db87c8d --- /dev/null +++ b/pkg/middlewares/aws_eip_association_expander.go @@ -0,0 +1,70 @@ +package middlewares + +import ( + "github.com/cloudskiff/driftctl/pkg/resource" + "github.com/cloudskiff/driftctl/pkg/resource/aws" +) + +/** + Fetching eip association from remote return every association but some of them are embedded in eip. + This middleware will check for every eip_association that here is no corresponding association_id inside eip. +*/ + +type EipAssociationExpander struct { + resourceFactory resource.ResourceFactory +} + +func NewEipAssociationExpander(resourceFactory resource.ResourceFactory) EipAssociationExpander { + return EipAssociationExpander{resourceFactory} +} + +func (m EipAssociationExpander) Execute(_, resourcesFromState *[]resource.Resource) error { + var newResources []resource.Resource + for _, res := range *resourcesFromState { + newResources = append(newResources, res) + + if res.TerraformType() != aws.AwsEipResourceType { + continue + } + if m.haveMatchingEipAssociation(res, resourcesFromState) { + continue + } + // This EIP have no association, check if we need to create one + assocID := res.Attributes().GetString("association_id") + if assocID == nil || *assocID == "" { + continue + } + + attributes := *res.Attributes() + newAssoc := m.resourceFactory.CreateAbstractResource( + aws.AwsEipAssociationResourceType, + *assocID, + map[string]interface{}{ + "allocation_id": res.TerraformId(), + "id": *assocID, + "instance_id": attributes["instance"], + "network_interface_id": attributes["network_interface"], + "private_ip_address": attributes["private_ip"], + "public_ip": attributes["public_ip"], + }, + ) + + newResources = append(newResources, newAssoc) + } + *resourcesFromState = newResources + + return nil +} + +func (m EipAssociationExpander) haveMatchingEipAssociation(cur resource.Resource, stateRes *[]resource.Resource) bool { + for _, res := range *stateRes { + if res.TerraformType() != aws.AwsEipAssociationResourceType { + continue + } + assocId := cur.Attributes().GetString("association_id") + if assocId != nil && res.TerraformId() == *assocId { + return true + } + } + return false +} diff --git a/pkg/resource/aws/aws_eip_association_test.go b/pkg/resource/aws/aws_eip_association_test.go new file mode 100644 index 00000000..10edfbcf --- /dev/null +++ b/pkg/resource/aws/aws_eip_association_test.go @@ -0,0 +1,30 @@ +package aws_test + +import ( + "testing" + + "github.com/cloudskiff/driftctl/test" + "github.com/cloudskiff/driftctl/test/acceptance" +) + +func TestAcc_Aws_EipAssociation(t *testing.T) { + acceptance.Run(t, acceptance.AccTestCase{ + TerraformVersion: "0.14.9", + Paths: []string{"./testdata/acc/aws_eip_association"}, + Args: []string{"scan", "--filter", "Type=='aws_eip' || Type=='aws_eip_association'"}, + Checks: []acceptance.AccCheck{ + { + Env: map[string]string{ + "AWS_REGION": "us-east-1", + }, + Check: func(result *test.ScanResult, stdout string, err error) { + if err != nil { + t.Fatal(err) + } + result.AssertInfrastructureIsInSync() + result.AssertManagedCount(1) + }, + }, + }, + }) +} diff --git a/pkg/resource/aws/testdata/acc/aws_eip_association/.terraform.lock.hcl b/pkg/resource/aws/testdata/acc/aws_eip_association/.terraform.lock.hcl new file mode 100644 index 00000000..19dd55fa --- /dev/null +++ b/pkg/resource/aws/testdata/acc/aws_eip_association/.terraform.lock.hcl @@ -0,0 +1,21 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.44.0" + constraints = "~> 3.44.0" + hashes = [ + "h1:hxQ8n9SHHfAIXd/FtfAqxokFYWBedzZf7xqQZWJajUs=", + "zh:0680315b29a140e9b7e4f5aeed3f2445abdfab31fc9237f34dcad06de4f410df", + "zh:13811322a205fb4a0ee617f0ae51ec94176befdf569235d0c7064db911f0acc7", + "zh:25e427a1cfcb1d411bc12040cf0684158d094416ecf18889a41196bacc761729", + "zh:40cd6acd24b060823f8d116355d8f844461a11925796b1757eb2ee18abc0bc7c", + "zh:94e2463eef555c388cd27f6e85ad803692d6d80ffa621bdc382ab119001d4de4", + "zh:aadc3bc216b14839e85b463f07b8507920ace5f202a608e4a835df23711c8a0d", + "zh:ab50dc1242af5a8fcdb18cf89beeaf2b2146b51ecfcecdbea033913a5f4c1c14", + "zh:ad48bbf4af66b5d48ca07c5c558d2f5724311db4dd943c1c98a7f3f107e03311", + "zh:ad76796c2145a7aaec1970a5244f5c0a9d200556121e2c5b382f296597b1a03c", + "zh:cf0a2181356598f8a2abfeaf0cdf385bdeea7f2e52821c850a2a08b60c26b9f6", + "zh:f76801af6bc34fe4a5bf1c63fa0204e24b81691049efecd6baa1526593f03935", + ] +} diff --git a/pkg/resource/aws/testdata/acc/aws_eip_association/mainf.tf b/pkg/resource/aws/testdata/acc/aws_eip_association/mainf.tf new file mode 100644 index 00000000..f0be5728 --- /dev/null +++ b/pkg/resource/aws/testdata/acc/aws_eip_association/mainf.tf @@ -0,0 +1,61 @@ +provider "aws" { + region = "us-east-1" +} +terraform { + required_providers { + aws = { + version = "~> 3.44.0" + } + } +} + +data "aws_ami" "ubuntu" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} + +resource "aws_instance" "web" { + ami = data.aws_ami.ubuntu.id + instance_type = "t3.micro" + subnet_id = aws_subnet.subnet-1.id + + tags = { + Name = "HelloWorld" + } +} + +resource "aws_eip" "lb" { + instance = aws_instance.web.id + vpc = true +// associate_with_private_ip = "10.0.0.12" + depends_on = [aws_internet_gateway.gw] +} + +resource "aws_vpc" "default" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true +} + +resource "aws_internet_gateway" "gw" { + vpc_id = aws_vpc.default.id +} + +resource "aws_subnet" "subnet-1" { + vpc_id = aws_vpc.default.id + cidr_block = "10.0.0.0/24" + map_public_ip_on_launch = true + + depends_on = [aws_internet_gateway.gw] +} +