diff --git a/.changelog/22137.txt b/.changelog/22137.txt new file mode 100644 index 00000000000..de3f346f580 --- /dev/null +++ b/.changelog/22137.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc_endpoint: Fix erroneous diffs in `policy` when no changes made or policies are equivalent +``` \ No newline at end of file diff --git a/internal/service/ec2/vpc_endpoint.go b/internal/service/ec2/vpc_endpoint.go index 3d9624afa48..b5cf2cf8cb8 100644 --- a/internal/service/ec2/vpc_endpoint.go +++ b/internal/service/ec2/vpc_endpoint.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + awspolicy "github.com/hashicorp/awspolicyequivalence" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -271,11 +272,21 @@ func resourceVPCEndpointRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting network_interface_ids: %s", err) } d.Set("owner_id", vpce.OwnerId) - policy, err := structure.NormalizeJsonString(aws.StringValue(vpce.PolicyDocument)) + + policyToSet, err := verify.SecondJSONUnlessEquivalent(d.Get("policy").(string), aws.StringValue(vpce.PolicyDocument)) + + if err != nil { + return fmt.Errorf("while setting policy (%s), encountered: %w", policyToSet, err) + } + + policyToSet, err = structure.NormalizeJsonString(policyToSet) + if err != nil { - return fmt.Errorf("policy contains an invalid JSON: %s", err) + return fmt.Errorf("policy (%s) is invalid JSON: %w", policyToSet, err) } - d.Set("policy", policy) + + d.Set("policy", policyToSet) + d.Set("private_dns_enabled", vpce.PrivateDnsEnabled) err = d.Set("route_table_ids", flex.FlattenStringSet(vpce.RouteTableIds)) if err != nil { @@ -326,15 +337,19 @@ func resourceVPCEndpointUpdate(d *schema.ResourceData, meta interface{}) error { } if d.HasChange("policy") { - policy, err := structure.NormalizeJsonString(d.Get("policy")) - if err != nil { - return fmt.Errorf("policy contains an invalid JSON: %s", err) - } - - if policy == "" { - req.ResetPolicy = aws.Bool(true) - } else { - req.PolicyDocument = aws.String(policy) + o, n := d.GetChange("policy") + + if equivalent, err := awspolicy.PoliciesAreEquivalent(o.(string), n.(string)); err != nil || !equivalent { + policy, err := structure.NormalizeJsonString(d.Get("policy")) + if err != nil { + return fmt.Errorf("policy contains an invalid JSON: %s", err) + } + + if policy == "" { + req.ResetPolicy = aws.Bool(true) + } else { + req.PolicyDocument = aws.String(policy) + } } } diff --git a/internal/service/ec2/vpc_endpoint_test.go b/internal/service/ec2/vpc_endpoint_test.go index 118e7871e8a..71ebc60b5dc 100644 --- a/internal/service/ec2/vpc_endpoint_test.go +++ b/internal/service/ec2/vpc_endpoint_test.go @@ -182,6 +182,31 @@ func TestAccEC2VPCEndpoint_gatewayPolicy(t *testing.T) { }) } +func TestAccEC2VPCEndpoint_ignoreEquivalent(t *testing.T) { + var endpoint ec2.VpcEndpoint + resourceName := "aws_vpc_endpoint.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckVpcEndpointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVpcEndpointOrderPolicyConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcEndpointExists(resourceName, &endpoint), + ), + }, + { + Config: testAccVpcEndpointNewOrderPolicyConfig(rName), + PlanOnly: true, + }, + }, + }) +} + func TestAccEC2VPCEndpoint_interfaceBasic(t *testing.T) { var endpoint ec2.VpcEndpoint resourceName := "aws_vpc_endpoint.test" @@ -1007,3 +1032,81 @@ resource "aws_vpc_endpoint" "test" { } `, rName)) } + +func testAccVpcEndpointOrderPolicyConfig(rName string) string { + return fmt.Sprintf(` +data "aws_vpc_endpoint_service" "test" { + service = "dynamodb" +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_vpc_endpoint" "test" { + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "ReadOnly" + Principal = "*" + Action = [ + "dynamodb:DescribeTable", + "dynamodb:ListTables", + "dynamodb:ListTagsOfResource", + ] + Effect = "Allow" + Resource = "*" + }] + }) + service_name = data.aws_vpc_endpoint_service.test.service_name + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName) +} + +func testAccVpcEndpointNewOrderPolicyConfig(rName string) string { + return fmt.Sprintf(` +data "aws_vpc_endpoint_service" "test" { + service = "dynamodb" +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_vpc_endpoint" "test" { + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "ReadOnly" + Principal = "*" + Action = [ + "dynamodb:ListTables", + "dynamodb:ListTagsOfResource", + "dynamodb:DescribeTable", + ] + Effect = "Allow" + Resource = "*" + }] + }) + service_name = data.aws_vpc_endpoint_service.test.service_name + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName) +}