diff --git a/.changelog/30375.txt b/.changelog/30375.txt new file mode 100644 index 00000000000..a3fe65ec090 --- /dev/null +++ b/.changelog/30375.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_dx_gateway: Add plan time validation to `name` argument +``` + +```release-note:enhancement +resource/aws_dx_gateway: Allow updates to `name` without forcing resource replacement +``` \ No newline at end of file diff --git a/internal/service/directconnect/gateway.go b/internal/service/directconnect/gateway.go index 522c2e4c381..7e41debdf30 100644 --- a/internal/service/directconnect/gateway.go +++ b/internal/service/directconnect/gateway.go @@ -3,6 +3,7 @@ package directconnect import ( "context" "log" + "regexp" "strconv" "time" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" ) // @SDKResource("aws_dx_gateway") @@ -21,6 +24,7 @@ func ResourceGateway() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceGatewayCreate, ReadWithoutTimeout: resourceGatewayRead, + UpdateWithoutTimeout: resourceGatewayUpdate, DeleteWithoutTimeout: resourceGatewayDelete, Importer: &schema.ResourceImporter{ @@ -32,15 +36,13 @@ func ResourceGateway() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validAmazonSideASN, + ValidateFunc: verify.ValidAmazonSideASN, }, - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-z0-9-]{1,100}$`), "Name must contain no more than 100 characters. Valid characters are a-z, 0-9, and hyphens (–)."), }, - "owner_account_id": { Type: schema.TypeString, Computed: true, @@ -64,22 +66,20 @@ func resourceGatewayCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.Get("amazon_side_asn").(string); ok && v != "" { - if v, err := strconv.ParseInt(v, 10, 64); err == nil { - input.AmazonSideAsn = aws.Int64(v) - } + v, _ := strconv.ParseInt(v, 10, 64) + input.AmazonSideAsn = aws.Int64(v) } - log.Printf("[DEBUG] Creating Direct Connect Gateway: %s", input) - resp, err := conn.CreateDirectConnectGatewayWithContext(ctx, input) + output, err := conn.CreateDirectConnectGatewayWithContext(ctx, input) if err != nil { return sdkdiag.AppendErrorf(diags, "creating Direct Connect Gateway (%s): %s", name, err) } - d.SetId(aws.StringValue(resp.DirectConnectGateway.DirectConnectGatewayId)) + d.SetId(aws.StringValue(output.DirectConnectGateway.DirectConnectGatewayId)) if _, err := waitGatewayCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for Direct Connect Gateway (%s) to create: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for Direct Connect Gateway (%s) create: %s", d.Id(), err) } return append(diags, resourceGatewayRead(ctx, d, meta)...) @@ -108,6 +108,26 @@ func resourceGatewayRead(ctx context.Context, d *schema.ResourceData, meta inter return diags } +func resourceGatewayUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).DirectConnectConn() + + if d.HasChange("name") { + input := &directconnect.UpdateDirectConnectGatewayInput{ + DirectConnectGatewayId: aws.String(d.Id()), + NewDirectConnectGatewayName: aws.String(d.Get("name").(string)), + } + + _, err := conn.UpdateDirectConnectGatewayWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating Direct Connect Gateway (%s): %s", d.Id(), err) + } + } + + return append(diags, resourceGatewayRead(ctx, d, meta)...) +} + func resourceGatewayDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).DirectConnectConn() @@ -126,7 +146,7 @@ func resourceGatewayDelete(ctx context.Context, d *schema.ResourceData, meta int } if _, err := waitGatewayDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for Direct Connect Gateway (%s) to delete: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for Direct Connect Gateway (%s) delete: %s", d.Id(), err) } return diags diff --git a/internal/service/directconnect/gateway_test.go b/internal/service/directconnect/gateway_test.go index b797117f28f..046355a4c11 100644 --- a/internal/service/directconnect/gateway_test.go +++ b/internal/service/directconnect/gateway_test.go @@ -98,6 +98,38 @@ func TestAccDirectConnectGateway_complex(t *testing.T) { }) } +func TestAccDirectConnectGateway_update(t *testing.T) { + ctx := acctest.Context(t) + var v directconnect.Gateway + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rBgpAsn := sdkacctest.RandIntRange(64512, 65534) + resourceName := "aws_dx_gateway.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, directconnect.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGatewayDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGatewayConfig_basic(rName1, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckGatewayExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "name", rName1), + ), + }, + { + Config: testAccGatewayConfig_basic(rName2, rBgpAsn), + Check: resource.ComposeTestCheckFunc( + testAccCheckGatewayExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "name", rName2), + ), + }, + }, + }) +} + func testAccCheckGatewayDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DirectConnectConn() diff --git a/internal/service/directconnect/validate.go b/internal/service/directconnect/validate.go index 8977bd72f4a..e54a5fb1f9c 100644 --- a/internal/service/directconnect/validate.go +++ b/internal/service/directconnect/validate.go @@ -1,9 +1,6 @@ package directconnect import ( - "fmt" - "strconv" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -22,24 +19,3 @@ func validConnectionBandWidth() schema.SchemaValidateFunc { "400Mbps", "500Mbps"}, false) } - -func validAmazonSideASN(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - - // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateVpnGateway.html - asn, err := strconv.ParseInt(value, 10, 64) - if err != nil { - errors = append(errors, fmt.Errorf("%q (%q) must be a 64-bit integer", k, v)) - return - } - - // https://github.com/hashicorp/terraform-provider-aws/issues/5263 - isLegacyAsn := func(a int64) bool { - return a == 7224 || a == 9059 || a == 10124 || a == 17493 - } - - if !isLegacyAsn(asn) && ((asn < 64512) || (asn > 65534 && asn < 4200000000) || (asn > 4294967294)) { - errors = append(errors, fmt.Errorf("%q (%q) must be 7224, 9059, 10124 or 17493 or in the range 64512 to 65534 or 4200000000 to 4294967294", k, v)) - } - return -} diff --git a/internal/service/directconnect/validate_test.go b/internal/service/directconnect/validate_test.go index 8741127265a..21acbafe432 100644 --- a/internal/service/directconnect/validate_test.go +++ b/internal/service/directconnect/validate_test.go @@ -42,49 +42,3 @@ func TestValidConnectionBandWidth(t *testing.T) { } } } - -func TestValidAmazonSideASN(t *testing.T) { - t.Parallel() - - validAsns := []string{ - "7224", - "9059", - "10124", - "17493", - "64512", - "64513", - "65533", - "65534", - "4200000000", - "4200000001", - "4294967293", - "4294967294", - } - for _, v := range validAsns { - _, errors := validAmazonSideASN(v, "amazon_side_asn") - if len(errors) != 0 { - t.Fatalf("%q should be a valid ASN: %q", v, errors) - } - } - - invalidAsns := []string{ - "1", - "ABCDEFG", - "", - "7225", - "9058", - "10125", - "17492", - "64511", - "65535", - "4199999999", - "4294967295", - "9999999999", - } - for _, v := range invalidAsns { - _, errors := validAmazonSideASN(v, "amazon_side_asn") - if len(errors) == 0 { - t.Fatalf("%q should be an invalid ASN", v) - } - } -} diff --git a/internal/service/ec2/validate.go b/internal/service/ec2/validate.go index abe4b81514f..830a1eb1132 100644 --- a/internal/service/ec2/validate.go +++ b/internal/service/ec2/validate.go @@ -3,7 +3,6 @@ package ec2 import ( "fmt" "regexp" - "strconv" "strings" ) @@ -43,24 +42,3 @@ func validNestedExactlyOneOf(m map[string]interface{}, valid []string) error { } return nil } - -func validAmazonSideASN(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - - // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateVpnGateway.html - asn, err := strconv.ParseInt(value, 10, 64) - if err != nil { - errors = append(errors, fmt.Errorf("%q (%q) must be a 64-bit integer", k, v)) - return - } - - // https://github.com/hashicorp/terraform-provider-aws/issues/5263 - isLegacyAsn := func(a int64) bool { - return a == 7224 || a == 9059 || a == 10124 || a == 17493 - } - - if !isLegacyAsn(asn) && ((asn < 64512) || (asn > 65534 && asn < 4200000000) || (asn > 4294967294)) { - errors = append(errors, fmt.Errorf("%q (%q) must be 7224, 9059, 10124 or 17493 or in the range 64512 to 65534 or 4200000000 to 4294967294", k, v)) - } - return -} diff --git a/internal/service/ec2/validate_test.go b/internal/service/ec2/validate_test.go index 632fb5e5fb0..121c7323a26 100644 --- a/internal/service/ec2/validate_test.go +++ b/internal/service/ec2/validate_test.go @@ -32,49 +32,3 @@ func TestValidSecurityGroupRuleDescription(t *testing.T) { } } } - -func TestValidAmazonSideASN(t *testing.T) { - t.Parallel() - - validAsns := []string{ - "7224", - "9059", - "10124", - "17493", - "64512", - "64513", - "65533", - "65534", - "4200000000", - "4200000001", - "4294967293", - "4294967294", - } - for _, v := range validAsns { - _, errors := validAmazonSideASN(v, "amazon_side_asn") - if len(errors) != 0 { - t.Fatalf("%q should be a valid ASN: %q", v, errors) - } - } - - invalidAsns := []string{ - "1", - "ABCDEFG", - "", - "7225", - "9058", - "10125", - "17492", - "64511", - "65535", - "4199999999", - "4294967295", - "9999999999", - } - for _, v := range invalidAsns { - _, errors := validAmazonSideASN(v, "amazon_side_asn") - if len(errors) == 0 { - t.Fatalf("%q should be an invalid ASN", v) - } - } -} diff --git a/internal/service/ec2/vpnsite_gateway.go b/internal/service/ec2/vpnsite_gateway.go index ca548a204f5..b8770a1c2fd 100644 --- a/internal/service/ec2/vpnsite_gateway.go +++ b/internal/service/ec2/vpnsite_gateway.go @@ -39,7 +39,7 @@ func ResourceVPNGateway() *schema.Resource { Optional: true, ForceNew: true, Computed: true, - ValidateFunc: validAmazonSideASN, + ValidateFunc: verify.ValidAmazonSideASN, }, "arn": { Type: schema.TypeString, diff --git a/internal/verify/validate.go b/internal/verify/validate.go index 85edeb16492..47e4edcb9ce 100644 --- a/internal/verify/validate.go +++ b/internal/verify/validate.go @@ -36,6 +36,27 @@ func Valid4ByteASN(v interface{}, k string) (ws []string, errors []error) { return } +func ValidAmazonSideASN(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateVpnGateway.html + asn, err := strconv.ParseInt(value, 10, 64) + if err != nil { + errors = append(errors, fmt.Errorf("%q (%q) must be a 64-bit integer", k, v)) + return + } + + // https://github.com/hashicorp/terraform-provider-aws/issues/5263 + isLegacyAsn := func(a int64) bool { + return a == 7224 || a == 9059 || a == 10124 || a == 17493 + } + + if !isLegacyAsn(asn) && ((asn < 64512) || (asn > 65534 && asn < 4200000000) || (asn > 4294967294)) { + errors = append(errors, fmt.Errorf("%q (%q) must be 7224, 9059, 10124 or 17493 or in the range 64512 to 65534 or 4200000000 to 4294967294", k, v)) + } + return +} + // ValidARN validates that a string value matches a generic ARN format var ValidARN = ValidARNCheck() diff --git a/internal/verify/validate_test.go b/internal/verify/validate_test.go index c05ddabf097..4100f9056af 100644 --- a/internal/verify/validate_test.go +++ b/internal/verify/validate_test.go @@ -9,6 +9,52 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +func TestValidAmazonSideASN(t *testing.T) { + t.Parallel() + + validAsns := []string{ + "7224", + "9059", + "10124", + "17493", + "64512", + "64513", + "65533", + "65534", + "4200000000", + "4200000001", + "4294967293", + "4294967294", + } + for _, v := range validAsns { + _, errors := ValidAmazonSideASN(v, "amazon_side_asn") + if len(errors) != 0 { + t.Fatalf("%q should be a valid ASN: %q", v, errors) + } + } + + invalidAsns := []string{ + "1", + "ABCDEFG", + "", + "7225", + "9058", + "10125", + "17492", + "64511", + "65535", + "4199999999", + "4294967295", + "9999999999", + } + for _, v := range invalidAsns { + _, errors := ValidAmazonSideASN(v, "amazon_side_asn") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid ASN", v) + } + } +} + func TestValid4ByteASNString(t *testing.T) { t.Parallel()