Skip to content

Commit b8b1ff8

Browse files
aditmenoerhancagirici
authored andcommitted
Fix API Gateway v1 external name oscillation causing perpetual delete/recreate
The 7 API Gateway v1 resources using FormattedIdentifierFromProvider("/", ...) had a mismatch between GetExternalNameFn (which reads tfstate["id"]) and GetIDFn (which joins parameters with "/"). The Terraform AWS provider stores internal IDs in a different format (prefixed, dash-separated) than the import format (slash-separated), causing the external-name annotation to change every reconciliation and triggering perpetual delete/recreate cycles and AWS 429 throttling. Fixes: #1974 Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
1 parent d6c6c20 commit b8b1ff8

File tree

1 file changed

+60
-7
lines changed

1 file changed

+60
-7
lines changed

config/externalname.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,17 +272,17 @@ var TerraformPluginSDKExternalNameConfigs = map[string]config.ExternalName{
272272
// API Gateway domain names can be imported using their name
273273
"aws_api_gateway_domain_name": config.IdentifierFromProvider,
274274
// aws_api_gateway_gateway_response can be imported using REST-API-ID/RESPONSE-TYPE
275-
"aws_api_gateway_gateway_response": FormattedIdentifierFromProvider("/", "rest_api_id", "response_type"),
275+
"aws_api_gateway_gateway_response": apiGatewayFormattedIdentifier("aggr", "rest_api_id", "response_type"),
276276
// aws_api_gateway_integration can be imported using REST-API-ID/RESOURCE-ID/HTTP-METHOD
277-
"aws_api_gateway_integration": FormattedIdentifierFromProvider("/", "rest_api_id", "resource_id", "http_method"),
277+
"aws_api_gateway_integration": apiGatewayFormattedIdentifier("agi", "rest_api_id", "resource_id", "http_method"),
278278
// aws_api_gateway_integration_response can be imported using REST-API-ID/RESOURCE-ID/HTTP-METHOD/STATUS-CODE
279-
"aws_api_gateway_integration_response": FormattedIdentifierFromProvider("/", "rest_api_id", "resource_id", "http_method", "status_code"),
279+
"aws_api_gateway_integration_response": apiGatewayFormattedIdentifier("agir", "rest_api_id", "resource_id", "http_method", "status_code"),
280280
// aws_api_gateway_method can be imported using REST-API-ID/RESOURCE-ID/HTTP-METHOD
281-
"aws_api_gateway_method": FormattedIdentifierFromProvider("/", "rest_api_id", "resource_id", "http_method"),
281+
"aws_api_gateway_method": apiGatewayFormattedIdentifier("agm", "rest_api_id", "resource_id", "http_method"),
282282
// aws_api_gateway_method_response can be imported using REST-API-ID/RESOURCE-ID/HTTP-METHOD/STATUS-CODE
283-
"aws_api_gateway_method_response": FormattedIdentifierFromProvider("/", "rest_api_id", "resource_id", "http_method", "status_code"),
283+
"aws_api_gateway_method_response": apiGatewayFormattedIdentifier("agmr", "rest_api_id", "resource_id", "http_method", "status_code"),
284284
// aws_api_gateway_method_settings can be imported using REST-API-ID/STAGE-NAME/METHOD-PATH
285-
"aws_api_gateway_method_settings": FormattedIdentifierFromProvider("/", "rest_api_id", "stage_name", "method_path"),
285+
"aws_api_gateway_method_settings": apiGatewayFormattedIdentifier("", "rest_api_id", "stage_name", "method_path"),
286286
// aws_api_gateway_model can be imported using REST-API-ID/NAME
287287
"aws_api_gateway_model": config.IdentifierFromProvider,
288288
// aws_api_gateway_request_validator can be imported using REST-API-ID/REQUEST-VALIDATOR-ID
@@ -294,7 +294,7 @@ var TerraformPluginSDKExternalNameConfigs = map[string]config.ExternalName{
294294
// aws_api_gateway_rest_api_policy can be imported by using the REST API ID
295295
"aws_api_gateway_rest_api_policy": FormattedIdentifierFromProvider("", "rest_api_id"),
296296
// aws_api_gateway_stage can be imported using REST-API-ID/STAGE-NAME
297-
"aws_api_gateway_stage": FormattedIdentifierFromProvider("/", "rest_api_id", "stage_name"),
297+
"aws_api_gateway_stage": apiGatewayFormattedIdentifier("ags", "rest_api_id", "stage_name"),
298298
// AWS API Gateway Usage Plan can be imported using the id
299299
"aws_api_gateway_usage_plan": config.IdentifierFromProvider,
300300
// AWS API Gateway Usage Plan Key can be imported using the USAGE-PLAN-ID/USAGE-PLAN-KEY-ID
@@ -3376,6 +3376,59 @@ func apiGatewayAccount() config.ExternalName {
33763376
return e
33773377
}
33783378

3379+
// apiGatewayFormattedIdentifier constructs external name configs for API Gateway
3380+
// v1 resources where the Terraform internal ID format (tfstate["id"]) differs
3381+
// from the import format. The internal format uses a prefix and dash separators
3382+
// (e.g., "agm-{restApiId}-{resourceId}-{httpMethod}") while the import format
3383+
// uses slash separators (e.g., "{restApiId}/{resourceId}/{httpMethod}").
3384+
// This function ensures GetExternalNameFn and GetIDFn produce consistent values
3385+
// to prevent external name oscillation. Pass an empty prefix for resources
3386+
// like method_settings that have no prefix.
3387+
func apiGatewayFormattedIdentifier(prefix string, keys ...string) config.ExternalName {
3388+
e := config.IdentifierFromProvider
3389+
e.GetExternalNameFn = func(tfstate map[string]interface{}) (string, error) {
3390+
id, ok := tfstate["id"]
3391+
if !ok {
3392+
return "", errors.New("id not found in tfstate")
3393+
}
3394+
idStr, ok := id.(string)
3395+
if !ok {
3396+
return "", errors.New("id is not a string")
3397+
}
3398+
// Strip the prefix (e.g., "agm-") if present
3399+
if prefix != "" {
3400+
idStr = strings.TrimPrefix(idStr, prefix+"-")
3401+
}
3402+
// Split into exactly len(keys) parts so the last component
3403+
// preserves any dashes it may contain (e.g., method_path)
3404+
parts := strings.SplitN(idStr, "-", len(keys))
3405+
if len(parts) != len(keys) {
3406+
return "", errors.Errorf("unexpected id format for %s: %s", prefix, id)
3407+
}
3408+
return strings.Join(parts, "/"), nil
3409+
}
3410+
e.GetIDFn = func(_ context.Context, _ string, parameters map[string]interface{}, _ map[string]interface{}) (string, error) {
3411+
vals := make([]string, len(keys))
3412+
for i, key := range keys {
3413+
val, ok := parameters[key]
3414+
if !ok {
3415+
return "", errors.Errorf("%s cannot be empty", key)
3416+
}
3417+
s, ok := val.(string)
3418+
if !ok {
3419+
return "", errors.Errorf("%s needs to be string", key)
3420+
}
3421+
vals[i] = s
3422+
}
3423+
id := strings.Join(vals, "-")
3424+
if prefix != "" {
3425+
return prefix + "-" + id, nil
3426+
}
3427+
return id, nil
3428+
}
3429+
return e
3430+
}
3431+
33793432
// fullARNTemplate builds a templated string for constructing a terraform id component which is an ARN, which includes
33803433
// the aws partition, service, region, account id, and resource. This is by far the most common form of ARN.
33813434
// e.g. arn:aws:ec2:ap-south-1:123456789012:instance/i-1234567890ab

0 commit comments

Comments
 (0)