diff --git a/README.md b/README.md index 31fa9b8..a6cb4e9 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ For upgrading AWS provider v4, some rules have not been implemented yet. The cur - [x] logging - [x] object_lock_configuration rule - [x] policy - - [ ] replication_configuration + - [x] replication_configuration - [x] request_payer - [x] server_side_encryption_configuration - [x] versioning diff --git a/filter/awsv4upgrade/aws_s3_bucket.go b/filter/awsv4upgrade/aws_s3_bucket.go index a61b34b..e050767 100644 --- a/filter/awsv4upgrade/aws_s3_bucket.go +++ b/filter/awsv4upgrade/aws_s3_bucket.go @@ -27,7 +27,7 @@ func NewAWSS3BucketFilter() editor.Filter { &AWSS3BucketLoggingFilter{}, &AWSS3BucketObjectLockConfigurationFilter{}, &AWSS3BucketPolicyFilter{}, - // &AWSS3BucketReplicationConfigurationFilter{}, + &AWSS3BucketReplicationConfigurationFilter{}, &AWSS3BucketRequestPayerFilter{}, &AWSS3BucketServerSideEncryptionConfigurationFilter{}, &AWSS3BucketVersioningFilter{}, diff --git a/filter/awsv4upgrade/aws_s3_bucket_replication_configuration.go b/filter/awsv4upgrade/aws_s3_bucket_replication_configuration.go new file mode 100644 index 0000000..19da491 --- /dev/null +++ b/filter/awsv4upgrade/aws_s3_bucket_replication_configuration.go @@ -0,0 +1,117 @@ +package awsv4upgrade + +import ( + "github.com/minamijoyo/tfedit/tfeditor" + "github.com/minamijoyo/tfedit/tfwrite" +) + +// AWSS3BucketReplicationConfigurationFilter is a filter implementation for +// upgrading the replication_configuration argument of aws_s3_bucket. +// https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-4-upgrade#replication_configuration-argument +type AWSS3BucketReplicationConfigurationFilter struct{} + +var _ tfeditor.ResourceFilter = (*AWSS3BucketReplicationConfigurationFilter)(nil) + +// NewAWSS3BucketReplicationConfigurationFilter creates a new instance of +// AWSS3BucketReplicationConfigurationFilter. +func NewAWSS3BucketReplicationConfigurationFilter() tfeditor.ResourceFilter { + return &AWSS3BucketReplicationConfigurationFilter{} +} + +// ResourceFilter upgrades the replication_configuration argument of +// aws_s3_bucket. +func (f *AWSS3BucketReplicationConfigurationFilter) ResourceFilter(inFile *tfwrite.File, resource *tfwrite.Resource) (*tfwrite.File, error) { + oldNestedBlock := "replication_configuration" + newResourceType := "aws_s3_bucket_replication_configuration" + + nestedBlocks := resource.FindNestedBlocksByType(oldNestedBlock) + if len(nestedBlocks) == 0 { + return inFile, nil + } + + resourceName := resource.Name() + newResource := tfwrite.NewEmptyResource(newResourceType, resourceName) + inFile.AppendResource(newResource) + setBucketArgument(newResource, resource) + + for _, nestedBlock := range nestedBlocks { + roleAttr := nestedBlock.GetAttribute("role") + if roleAttr != nil { + newResource.AppendAttribute(roleAttr) + } + + rulesBlocks := nestedBlock.FindNestedBlocksByType("rules") + for _, rulesBlock := range rulesBlocks { + // Rename a `rules` block to a `rule` block + rulesBlock.SetType("rule") + newResource.AppendNestedBlock(rulesBlock) + + // Map a `delete_marker_replication_status` attribute to a `delete_marker_replication` block + // delete_marker_replication_status = "Enabled" + // => + // delete_marker_replication { + // status = "Enabled" + // } + deleteMarkerAttr := rulesBlock.GetAttribute("delete_marker_replication_status") + if deleteMarkerAttr != nil { + deleteMarkerBlock := tfwrite.NewEmptyNestedBlock("delete_marker_replication") + rulesBlock.AppendNestedBlock(deleteMarkerBlock) + deleteMarkerBlock.SetAttributeRaw("status", deleteMarkerAttr.ValueAsTokens()) + rulesBlock.RemoveAttribute("delete_marker_replication_status") + } + + destinationBlocks := rulesBlock.FindNestedBlocksByType("destination") + for _, destinationBlock := range destinationBlocks { + // Map a `replication_time.minutes` attribute to a `replication_time.time.minutes` attribute + // replication_time { + // status = "Enabled" + // minutes = 15 + // } + // => + // replication_time { + // status = "Enabled" + // time { + // minutes = 15 + // } + // } + replicationTimeBlocks := destinationBlock.FindNestedBlocksByType("replication_time") + for _, replicationTimeBlock := range replicationTimeBlocks { + minutesAttribute := replicationTimeBlock.GetAttribute("minutes") + if minutesAttribute != nil { + timeBlock := tfwrite.NewEmptyNestedBlock("time") + replicationTimeBlock.AppendNestedBlock(timeBlock) + timeBlock.SetAttributeRaw("minutes", minutesAttribute.ValueAsTokens()) + replicationTimeBlock.RemoveAttribute("minutes") + } + } + + // Map a `metrics.minutes` attribute to a `metrics.event_threshold.minutes` attribute + // metrics { + // status = "Enabled" + // minutes = 15 + // } + // => + // metrics { + // status = "Enabled" + // event_threshold { + // minutes = 15 + // } + // } + metricsBlocks := destinationBlock.FindNestedBlocksByType("metrics") + for _, metricsBlock := range metricsBlocks { + minutesAttribute := metricsBlock.GetAttribute("minutes") + if minutesAttribute != nil { + eventThresholdBlock := tfwrite.NewEmptyNestedBlock("event_threshold") + metricsBlock.AppendNestedBlock(eventThresholdBlock) + eventThresholdBlock.SetAttributeRaw("minutes", minutesAttribute.ValueAsTokens()) + metricsBlock.RemoveAttribute("minutes") + } + } + } + } + + resource.RemoveNestedBlock(nestedBlock) + } + + return inFile, nil +} diff --git a/filter/awsv4upgrade/aws_s3_bucket_replication_configuration_test.go b/filter/awsv4upgrade/aws_s3_bucket_replication_configuration_test.go new file mode 100644 index 0000000..7b950a1 --- /dev/null +++ b/filter/awsv4upgrade/aws_s3_bucket_replication_configuration_test.go @@ -0,0 +1,133 @@ +package awsv4upgrade + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/minamijoyo/hcledit/editor" + "github.com/minamijoyo/tfedit/tfeditor" +) + +func TestAWSS3BucketReplicationConfigurationFilter(t *testing.T) { + cases := []struct { + name string + src string + ok bool + want string + }{ + { + name: "simple", + src: ` +resource "aws_s3_bucket" "example" { + bucket = "tfedit-test" + + replication_configuration { + role = aws_iam_role.replication.arn + rules { + id = "foobar" + status = "Enabled" + + filter {} + delete_marker_replication_status = "Enabled" + + destination { + bucket = aws_s3_bucket.destination.arn + storage_class = "STANDARD" + + replication_time { + status = "Enabled" + minutes = 15 + } + + metrics { + status = "Enabled" + minutes = 15 + } + } + } + } +} +`, + ok: true, + want: ` +resource "aws_s3_bucket" "example" { + bucket = "tfedit-test" + +} + +resource "aws_s3_bucket_replication_configuration" "example" { + bucket = aws_s3_bucket.example.id + role = aws_iam_role.replication.arn + + rule { + id = "foobar" + status = "Enabled" + + filter {} + + destination { + bucket = aws_s3_bucket.destination.arn + storage_class = "STANDARD" + + replication_time { + status = "Enabled" + + time { + minutes = 15 + } + } + + metrics { + status = "Enabled" + + event_threshold { + minutes = 15 + } + } + } + + delete_marker_replication { + status = "Enabled" + } + } +} +`, + }, + { + name: "argument not found", + src: ` +resource "aws_s3_bucket" "example" { + bucket = "tfedit-test" + foo {} +} +`, + ok: true, + want: ` +resource "aws_s3_bucket" "example" { + bucket = "tfedit-test" + foo {} +} +`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + filter := &AWSS3BucketFilter{filters: []tfeditor.ResourceFilter{&AWSS3BucketReplicationConfigurationFilter{}}} + o := editor.NewEditOperator(filter) + output, err := o.Apply([]byte(tc.src), "test") + if tc.ok && err != nil { + t.Fatalf("unexpected err = %s", err) + } + + got := string(output) + if !tc.ok && err == nil { + t.Fatalf("expected to return an error, but no error, outStream: \n%s", got) + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Fatalf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, tc.want, diff) + } + }) + } +} diff --git a/filter/awsv4upgrade/aws_s3_bucket_test.go b/filter/awsv4upgrade/aws_s3_bucket_test.go index 23ff203..06a1f1e 100644 --- a/filter/awsv4upgrade/aws_s3_bucket_test.go +++ b/filter/awsv4upgrade/aws_s3_bucket_test.go @@ -204,6 +204,32 @@ resource "aws_s3_bucket" "example" { } EOF + replication_configuration { + role = aws_iam_role.replication.arn + rules { + id = "foobar" + status = "Enabled" + + filter {} + delete_marker_replication_status = "Enabled" + + destination { + bucket = aws_s3_bucket.destination.arn + storage_class = "STANDARD" + + replication_time { + status = "Enabled" + minutes = 15 + } + + metrics { + status = "Enabled" + minutes = 15 + } + } + } + } + request_payer = "Requester" server_side_encryption_configuration { @@ -332,6 +358,43 @@ resource "aws_s3_bucket_policy" "example" { EOF } +resource "aws_s3_bucket_replication_configuration" "example" { + bucket = aws_s3_bucket.example.id + role = aws_iam_role.replication.arn + + rule { + id = "foobar" + status = "Enabled" + + filter {} + + destination { + bucket = aws_s3_bucket.destination.arn + storage_class = "STANDARD" + + replication_time { + status = "Enabled" + + time { + minutes = 15 + } + } + + metrics { + status = "Enabled" + + event_threshold { + minutes = 15 + } + } + } + + delete_marker_replication { + status = "Enabled" + } + } +} + resource "aws_s3_bucket_request_payment_configuration" "example" { bucket = aws_s3_bucket.example.id payer = "Requester"