From c9e3efb966d17823447ef7bd175d1a14592f6ad5 Mon Sep 17 00:00:00 2001 From: warren830 Date: Fri, 4 Sep 2026 07:38:41 +0000 Subject: [PATCH 1/2] fix: include API stage variables in the deployment hash ApiGatewayDeployment.make_auto_deployable hashed the swagger body, OpenAPI version, custom domain and function names, but not the stage's Variables. A variables-only change therefore produced the same deployment logical id, so no new deployment resource was created. Two consequences: the new stage variables were never deployed, and because the UpdateStage call points the stage back at the deployment it already references, any deployment made outside SAM since the last SAM deployment was reverted. Stage variables are now part of the hash, but only when set, so templates without stage variables keep their existing deployment logical id. Fixes #3703 --- samtranslator/model/apigateway.py | 8 ++++ ..._with_openapi_definition_body_no_flag.json | 12 ++--- ...pi_with_swagger_and_openapi_with_auth.json | 12 ++--- ..._with_openapi_definition_body_no_flag.json | 12 ++--- ...pi_with_swagger_and_openapi_with_auth.json | 12 ++--- .../output/aws-cn/explicit_api.json | 6 +-- .../output/aws-cn/explicit_api_openapi_3.json | 6 +-- ...function_with_alias_and_event_sources.json | 6 +-- .../global_handle_path_level_parameter.json | 12 ++--- .../output/aws-cn/globals_for_api.json | 12 ++--- .../output/aws-cn/intrinsic_functions.json | 6 +-- ..._with_openapi_definition_body_no_flag.json | 12 ++--- ...pi_with_swagger_and_openapi_with_auth.json | 12 ++--- .../output/aws-us-gov/explicit_api.json | 6 +-- .../aws-us-gov/explicit_api_openapi_3.json | 6 +-- ...function_with_alias_and_event_sources.json | 6 +-- .../global_handle_path_level_parameter.json | 12 ++--- .../output/aws-us-gov/globals_for_api.json | 12 ++--- .../aws-us-gov/intrinsic_functions.json | 6 +-- tests/translator/output/explicit_api.json | 6 +-- .../output/explicit_api_openapi_3.json | 6 +-- ...function_with_alias_and_event_sources.json | 6 +-- .../global_handle_path_level_parameter.json | 12 ++--- tests/translator/output/globals_for_api.json | 12 ++--- .../output/intrinsic_functions.json | 6 +-- tests/translator/test_api_resource.py | 47 +++++++++++++++++++ 26 files changed, 163 insertions(+), 108 deletions(-) diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 5d9d447039..6fa10b5827 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -131,6 +131,14 @@ def make_auto_deployable( hash_input.append(str(openapi_version)) if domain: hash_input.append(json.dumps(domain)) + # Stage variables are applied with an UpdateStage call, which points the stage back at the + # deployment it already references. If they are not part of this hash, a variables-only + # change reuses the existing deployment: the new variables are never deployed, and any + # deployment made outside SAM since the last SAM deployment is reverted. + # Only added when set, so templates without stage variables keep their existing hash. + stage_variables = getattr(stage, "Variables", None) + if stage_variables: + hash_input.append(json.dumps(stage_variables, sort_keys=True)) function_names = redeploy_restapi_parameters.get("function_names") if redeploy_restapi_parameters else None # The deployment logical id is + "Deployment" # The keyword "Deployment" is removed and all the function names associated with api is obtained diff --git a/tests/translator/output/api_with_openapi_definition_body_no_flag.json b/tests/translator/output/api_with_openapi_definition_body_no_flag.json index b7904447cd..c6e9e62b75 100644 --- a/tests/translator/output/api_with_openapi_definition_body_no_flag.json +++ b/tests/translator/output/api_with_openapi_definition_body_no_flag.json @@ -55,9 +55,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9252467a1e": { + "ExplicitApiDeploymente24cb02185": { "Properties": { - "Description": "RestApi deployment id: 9252467a1edc49ba35cb258640f5e3734cc9fab1", + "Description": "RestApi deployment id: e24cb0218583daf7c5e537a4e639dfb3a2971fdf", "RestApiId": { "Ref": "ExplicitApi" }, @@ -70,7 +70,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9252467a1e" + "Ref": "ExplicitApiDeploymente24cb02185" }, "RestApiId": { "Ref": "ExplicitApi" @@ -230,9 +230,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentdb4b9da82a": { + "ServerlessRestApiDeployment62318f9715": { "Properties": { - "Description": "RestApi deployment id: db4b9da82adc6031fcd32bf3a4954485464fc009", + "Description": "RestApi deployment id: 62318f9715b2b5932b597575f6a4a0eafd0cf7e3", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -245,7 +245,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentdb4b9da82a" + "Ref": "ServerlessRestApiDeployment62318f9715" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/api_with_swagger_and_openapi_with_auth.json b/tests/translator/output/api_with_swagger_and_openapi_with_auth.json index d215e100d3..73d5ccb494 100644 --- a/tests/translator/output/api_with_swagger_and_openapi_with_auth.json +++ b/tests/translator/output/api_with_swagger_and_openapi_with_auth.json @@ -54,9 +54,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment7c4f7dda23": { + "ExplicitApiDeployment8e20079165": { "Properties": { - "Description": "RestApi deployment id: 7c4f7dda23acd71e4a653861510d82ad7809e562", + "Description": "RestApi deployment id: 8e20079165a6421b14f28fc3f812a282e9ca3d90", "RestApiId": { "Ref": "ExplicitApi" }, @@ -69,7 +69,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment7c4f7dda23" + "Ref": "ExplicitApiDeployment8e20079165" }, "RestApiId": { "Ref": "ExplicitApi" @@ -267,9 +267,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymente091b9b9a0": { + "ServerlessRestApiDeployment26fa62bf10": { "Properties": { - "Description": "RestApi deployment id: e091b9b9a0d7b8b9db6fee8c4ad295eb98edde08", + "Description": "RestApi deployment id: 26fa62bf10190a228c478ca0189e786b42e35acd", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -282,7 +282,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymente091b9b9a0" + "Ref": "ServerlessRestApiDeployment26fa62bf10" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-cn/api_with_openapi_definition_body_no_flag.json b/tests/translator/output/aws-cn/api_with_openapi_definition_body_no_flag.json index ca6140b3fb..6f4b1f3cb9 100644 --- a/tests/translator/output/aws-cn/api_with_openapi_definition_body_no_flag.json +++ b/tests/translator/output/aws-cn/api_with_openapi_definition_body_no_flag.json @@ -63,9 +63,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9252467a1e": { + "ExplicitApiDeploymente24cb02185": { "Properties": { - "Description": "RestApi deployment id: 9252467a1edc49ba35cb258640f5e3734cc9fab1", + "Description": "RestApi deployment id: e24cb0218583daf7c5e537a4e639dfb3a2971fdf", "RestApiId": { "Ref": "ExplicitApi" }, @@ -78,7 +78,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9252467a1e" + "Ref": "ExplicitApiDeploymente24cb02185" }, "RestApiId": { "Ref": "ExplicitApi" @@ -246,9 +246,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymente1212668e0": { + "ServerlessRestApiDeployment409ed00828": { "Properties": { - "Description": "RestApi deployment id: e1212668e096994ab32167666f5a877bd6ac5fad", + "Description": "RestApi deployment id: 409ed008280bb6489cdd1707ff7f2b66d5ad40bb", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -261,7 +261,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymente1212668e0" + "Ref": "ServerlessRestApiDeployment409ed00828" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-cn/api_with_swagger_and_openapi_with_auth.json b/tests/translator/output/aws-cn/api_with_swagger_and_openapi_with_auth.json index 246a43aef2..92be58e118 100644 --- a/tests/translator/output/aws-cn/api_with_swagger_and_openapi_with_auth.json +++ b/tests/translator/output/aws-cn/api_with_swagger_and_openapi_with_auth.json @@ -62,9 +62,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment7c4f7dda23": { + "ExplicitApiDeployment8e20079165": { "Properties": { - "Description": "RestApi deployment id: 7c4f7dda23acd71e4a653861510d82ad7809e562", + "Description": "RestApi deployment id: 8e20079165a6421b14f28fc3f812a282e9ca3d90", "RestApiId": { "Ref": "ExplicitApi" }, @@ -77,7 +77,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment7c4f7dda23" + "Ref": "ExplicitApiDeployment8e20079165" }, "RestApiId": { "Ref": "ExplicitApi" @@ -283,9 +283,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeployment614ec93e15": { + "ServerlessRestApiDeploymente8a33c953e": { "Properties": { - "Description": "RestApi deployment id: 614ec93e159f50797f73789c416660484070ee2e", + "Description": "RestApi deployment id: e8a33c953ef3abf412bc61a49eecc59a9eb71bc8", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -298,7 +298,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeployment614ec93e15" + "Ref": "ServerlessRestApiDeploymente8a33c953e" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-cn/explicit_api.json b/tests/translator/output/aws-cn/explicit_api.json index 63eefda8aa..6118e3c579 100644 --- a/tests/translator/output/aws-cn/explicit_api.json +++ b/tests/translator/output/aws-cn/explicit_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-cn/explicit_api_openapi_3.json b/tests/translator/output/aws-cn/explicit_api_openapi_3.json index 36f676857e..6a2663a4fc 100644 --- a/tests/translator/output/aws-cn/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-cn/explicit_api_openapi_3.json @@ -68,9 +68,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-cn/function_with_alias_and_event_sources.json b/tests/translator/output/aws-cn/function_with_alias_and_event_sources.json index b61186ff13..45c4830d40 100644 --- a/tests/translator/output/aws-cn/function_with_alias_and_event_sources.json +++ b/tests/translator/output/aws-cn/function_with_alias_and_event_sources.json @@ -24,9 +24,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment2acb558823": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 2acb558823d4068b07590e8cd40633b8597a3157", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -37,7 +37,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment2acb558823" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-cn/global_handle_path_level_parameter.json b/tests/translator/output/aws-cn/global_handle_path_level_parameter.json index c529037537..180c9aa90c 100644 --- a/tests/translator/output/aws-cn/global_handle_path_level_parameter.json +++ b/tests/translator/output/aws-cn/global_handle_path_level_parameter.json @@ -70,9 +70,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9a254aa466": { + "ExplicitApiDeployment31e389dd75": { "Properties": { - "Description": "RestApi deployment id: 9a254aa466c6f818951dfb6e45fde65489beb153", + "Description": "RestApi deployment id: 31e389dd75bba46beb7a57c4256c48c5a9b127f4", "RestApiId": { "Ref": "ExplicitApi" }, @@ -85,7 +85,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9a254aa466" + "Ref": "ExplicitApiDeployment31e389dd75" }, "RestApiId": { "Ref": "ExplicitApi" @@ -253,9 +253,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymente1212668e0": { + "ServerlessRestApiDeployment409ed00828": { "Properties": { - "Description": "RestApi deployment id: e1212668e096994ab32167666f5a877bd6ac5fad", + "Description": "RestApi deployment id: 409ed008280bb6489cdd1707ff7f2b66d5ad40bb", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -268,7 +268,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymente1212668e0" + "Ref": "ServerlessRestApiDeployment409ed00828" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-cn/globals_for_api.json b/tests/translator/output/aws-cn/globals_for_api.json index ee86381d30..eff4c07730 100644 --- a/tests/translator/output/aws-cn/globals_for_api.json +++ b/tests/translator/output/aws-cn/globals_for_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment43e01e673d": { + "ExplicitApiDeployment0b29d48047": { "Properties": { - "Description": "RestApi deployment id: 43e01e673d7acbd09e4c38ff78dd6ddaf2ed1d55", + "Description": "RestApi deployment id: 0b29d480473c478bb1a2337fdc7ff0b80bbe48cd", "RestApiId": { "Ref": "ExplicitApi" }, @@ -84,7 +84,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment43e01e673d" + "Ref": "ExplicitApiDeployment0b29d48047" }, "RestApiId": { "Ref": "ExplicitApi" @@ -260,9 +260,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentf6c326a165": { + "ServerlessRestApiDeployment3556755163": { "Properties": { - "Description": "RestApi deployment id: f6c326a1656cc9fbb0106cc645598d88575554eb", + "Description": "RestApi deployment id: 3556755163aac22f1710a4c6babf8de8932e55b6", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -275,7 +275,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentf6c326a165" + "Ref": "ServerlessRestApiDeployment3556755163" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-cn/intrinsic_functions.json b/tests/translator/output/aws-cn/intrinsic_functions.json index 7a93d4676d..b91054164f 100644 --- a/tests/translator/output/aws-cn/intrinsic_functions.json +++ b/tests/translator/output/aws-cn/intrinsic_functions.json @@ -259,9 +259,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "MyExplicitApiDeployment7145dd00ce": { + "MyExplicitApiDeployment011ab6fdbf": { "Properties": { - "Description": "RestApi deployment id: 7145dd00cea59b4a62b4d7855add490c587f3f62", + "Description": "RestApi deployment id: 011ab6fdbfec2a0b749690c4c389b4f44e51a053", "RestApiId": { "Ref": "MyExplicitApi" }, @@ -272,7 +272,7 @@ "MyExplicitApidevStage": { "Properties": { "DeploymentId": { - "Ref": "MyExplicitApiDeployment7145dd00ce" + "Ref": "MyExplicitApiDeployment011ab6fdbf" }, "RestApiId": { "Ref": "MyExplicitApi" diff --git a/tests/translator/output/aws-us-gov/api_with_openapi_definition_body_no_flag.json b/tests/translator/output/aws-us-gov/api_with_openapi_definition_body_no_flag.json index d95ca5396d..c3086b1754 100644 --- a/tests/translator/output/aws-us-gov/api_with_openapi_definition_body_no_flag.json +++ b/tests/translator/output/aws-us-gov/api_with_openapi_definition_body_no_flag.json @@ -63,9 +63,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9252467a1e": { + "ExplicitApiDeploymente24cb02185": { "Properties": { - "Description": "RestApi deployment id: 9252467a1edc49ba35cb258640f5e3734cc9fab1", + "Description": "RestApi deployment id: e24cb0218583daf7c5e537a4e639dfb3a2971fdf", "RestApiId": { "Ref": "ExplicitApi" }, @@ -78,7 +78,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9252467a1e" + "Ref": "ExplicitApiDeploymente24cb02185" }, "RestApiId": { "Ref": "ExplicitApi" @@ -246,9 +246,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentc969c99f9d": { + "ServerlessRestApiDeployment6ba28851eb": { "Properties": { - "Description": "RestApi deployment id: c969c99f9d6b6921dff605a206e8989bdb7d1bc7", + "Description": "RestApi deployment id: 6ba28851eb50150f1e4349e33bc5185400c063d9", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -261,7 +261,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentc969c99f9d" + "Ref": "ServerlessRestApiDeployment6ba28851eb" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-us-gov/api_with_swagger_and_openapi_with_auth.json b/tests/translator/output/aws-us-gov/api_with_swagger_and_openapi_with_auth.json index 103de42472..3648f6d177 100644 --- a/tests/translator/output/aws-us-gov/api_with_swagger_and_openapi_with_auth.json +++ b/tests/translator/output/aws-us-gov/api_with_swagger_and_openapi_with_auth.json @@ -62,9 +62,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment7c4f7dda23": { + "ExplicitApiDeployment8e20079165": { "Properties": { - "Description": "RestApi deployment id: 7c4f7dda23acd71e4a653861510d82ad7809e562", + "Description": "RestApi deployment id: 8e20079165a6421b14f28fc3f812a282e9ca3d90", "RestApiId": { "Ref": "ExplicitApi" }, @@ -77,7 +77,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment7c4f7dda23" + "Ref": "ExplicitApiDeployment8e20079165" }, "RestApiId": { "Ref": "ExplicitApi" @@ -283,9 +283,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeployment05a3d3687d": { + "ServerlessRestApiDeployment9d5c248b84": { "Properties": { - "Description": "RestApi deployment id: 05a3d3687d4bfb804b237d40817879c6559ac61c", + "Description": "RestApi deployment id: 9d5c248b84fd7e0c6d26463880550ef124b0e145", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -298,7 +298,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeployment05a3d3687d" + "Ref": "ServerlessRestApiDeployment9d5c248b84" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-us-gov/explicit_api.json b/tests/translator/output/aws-us-gov/explicit_api.json index 14ff0657f1..35836df5a8 100644 --- a/tests/translator/output/aws-us-gov/explicit_api.json +++ b/tests/translator/output/aws-us-gov/explicit_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json index 5383290e61..f53d697d9c 100644 --- a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json @@ -68,9 +68,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-us-gov/function_with_alias_and_event_sources.json b/tests/translator/output/aws-us-gov/function_with_alias_and_event_sources.json index 35a2609b22..585ac2cd1e 100644 --- a/tests/translator/output/aws-us-gov/function_with_alias_and_event_sources.json +++ b/tests/translator/output/aws-us-gov/function_with_alias_and_event_sources.json @@ -24,9 +24,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment2acb558823": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 2acb558823d4068b07590e8cd40633b8597a3157", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -37,7 +37,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment2acb558823" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-us-gov/global_handle_path_level_parameter.json b/tests/translator/output/aws-us-gov/global_handle_path_level_parameter.json index 633d47f15d..82127cdf28 100644 --- a/tests/translator/output/aws-us-gov/global_handle_path_level_parameter.json +++ b/tests/translator/output/aws-us-gov/global_handle_path_level_parameter.json @@ -70,9 +70,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9a254aa466": { + "ExplicitApiDeployment31e389dd75": { "Properties": { - "Description": "RestApi deployment id: 9a254aa466c6f818951dfb6e45fde65489beb153", + "Description": "RestApi deployment id: 31e389dd75bba46beb7a57c4256c48c5a9b127f4", "RestApiId": { "Ref": "ExplicitApi" }, @@ -85,7 +85,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9a254aa466" + "Ref": "ExplicitApiDeployment31e389dd75" }, "RestApiId": { "Ref": "ExplicitApi" @@ -253,9 +253,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentc969c99f9d": { + "ServerlessRestApiDeployment6ba28851eb": { "Properties": { - "Description": "RestApi deployment id: c969c99f9d6b6921dff605a206e8989bdb7d1bc7", + "Description": "RestApi deployment id: 6ba28851eb50150f1e4349e33bc5185400c063d9", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -268,7 +268,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentc969c99f9d" + "Ref": "ServerlessRestApiDeployment6ba28851eb" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-us-gov/globals_for_api.json b/tests/translator/output/aws-us-gov/globals_for_api.json index 0f1844e2ca..4edd571c5b 100644 --- a/tests/translator/output/aws-us-gov/globals_for_api.json +++ b/tests/translator/output/aws-us-gov/globals_for_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment43e01e673d": { + "ExplicitApiDeployment0b29d48047": { "Properties": { - "Description": "RestApi deployment id: 43e01e673d7acbd09e4c38ff78dd6ddaf2ed1d55", + "Description": "RestApi deployment id: 0b29d480473c478bb1a2337fdc7ff0b80bbe48cd", "RestApiId": { "Ref": "ExplicitApi" }, @@ -84,7 +84,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment43e01e673d" + "Ref": "ExplicitApiDeployment0b29d48047" }, "RestApiId": { "Ref": "ExplicitApi" @@ -260,9 +260,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeployment6fd1928d9b": { + "ServerlessRestApiDeployment0a9707dedb": { "Properties": { - "Description": "RestApi deployment id: 6fd1928d9b9ad3c711a371e1337306458029f8bd", + "Description": "RestApi deployment id: 0a9707dedb5b0b282252fb37d74c2803a4ea3d1e", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -275,7 +275,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeployment6fd1928d9b" + "Ref": "ServerlessRestApiDeployment0a9707dedb" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/aws-us-gov/intrinsic_functions.json b/tests/translator/output/aws-us-gov/intrinsic_functions.json index b2b44466be..e5159d8afe 100644 --- a/tests/translator/output/aws-us-gov/intrinsic_functions.json +++ b/tests/translator/output/aws-us-gov/intrinsic_functions.json @@ -259,9 +259,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "MyExplicitApiDeployment7145dd00ce": { + "MyExplicitApiDeployment011ab6fdbf": { "Properties": { - "Description": "RestApi deployment id: 7145dd00cea59b4a62b4d7855add490c587f3f62", + "Description": "RestApi deployment id: 011ab6fdbfec2a0b749690c4c389b4f44e51a053", "RestApiId": { "Ref": "MyExplicitApi" }, @@ -272,7 +272,7 @@ "MyExplicitApidevStage": { "Properties": { "DeploymentId": { - "Ref": "MyExplicitApiDeployment7145dd00ce" + "Ref": "MyExplicitApiDeployment011ab6fdbf" }, "RestApiId": { "Ref": "MyExplicitApi" diff --git a/tests/translator/output/explicit_api.json b/tests/translator/output/explicit_api.json index 95c019102d..cbc7dc9d1f 100644 --- a/tests/translator/output/explicit_api.json +++ b/tests/translator/output/explicit_api.json @@ -53,9 +53,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -66,7 +66,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/explicit_api_openapi_3.json b/tests/translator/output/explicit_api_openapi_3.json index 5aac47a166..99e84cfefa 100644 --- a/tests/translator/output/explicit_api_openapi_3.json +++ b/tests/translator/output/explicit_api_openapi_3.json @@ -52,9 +52,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment03f2a64f84": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -65,7 +65,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment03f2a64f84" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/function_with_alias_and_event_sources.json b/tests/translator/output/function_with_alias_and_event_sources.json index 574336b382..792b00b414 100644 --- a/tests/translator/output/function_with_alias_and_event_sources.json +++ b/tests/translator/output/function_with_alias_and_event_sources.json @@ -16,9 +16,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeployment2acb558823": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: 2acb558823d4068b07590e8cd40633b8597a3157", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -29,7 +29,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeployment2acb558823" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/global_handle_path_level_parameter.json b/tests/translator/output/global_handle_path_level_parameter.json index 1eed923460..406eeebe81 100644 --- a/tests/translator/output/global_handle_path_level_parameter.json +++ b/tests/translator/output/global_handle_path_level_parameter.json @@ -62,9 +62,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment9a254aa466": { + "ExplicitApiDeployment31e389dd75": { "Properties": { - "Description": "RestApi deployment id: 9a254aa466c6f818951dfb6e45fde65489beb153", + "Description": "RestApi deployment id: 31e389dd75bba46beb7a57c4256c48c5a9b127f4", "RestApiId": { "Ref": "ExplicitApi" }, @@ -77,7 +77,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment9a254aa466" + "Ref": "ExplicitApiDeployment31e389dd75" }, "RestApiId": { "Ref": "ExplicitApi" @@ -237,9 +237,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentdb4b9da82a": { + "ServerlessRestApiDeployment62318f9715": { "Properties": { - "Description": "RestApi deployment id: db4b9da82adc6031fcd32bf3a4954485464fc009", + "Description": "RestApi deployment id: 62318f9715b2b5932b597575f6a4a0eafd0cf7e3", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -252,7 +252,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentdb4b9da82a" + "Ref": "ServerlessRestApiDeployment62318f9715" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/globals_for_api.json b/tests/translator/output/globals_for_api.json index 6c624d7f4e..5ed2520570 100644 --- a/tests/translator/output/globals_for_api.json +++ b/tests/translator/output/globals_for_api.json @@ -61,9 +61,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ExplicitApiDeployment43e01e673d": { + "ExplicitApiDeployment0b29d48047": { "Properties": { - "Description": "RestApi deployment id: 43e01e673d7acbd09e4c38ff78dd6ddaf2ed1d55", + "Description": "RestApi deployment id: 0b29d480473c478bb1a2337fdc7ff0b80bbe48cd", "RestApiId": { "Ref": "ExplicitApi" }, @@ -76,7 +76,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ExplicitApiDeployment43e01e673d" + "Ref": "ExplicitApiDeployment0b29d48047" }, "RestApiId": { "Ref": "ExplicitApi" @@ -244,9 +244,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ServerlessRestApiDeploymentaa32438b68": { + "ServerlessRestApiDeployment56105d1476": { "Properties": { - "Description": "RestApi deployment id: aa32438b68e05d3771a975585dfbc7b012672b55", + "Description": "RestApi deployment id: 56105d1476c2b2ba36a1c595f0c9f323feb5efd9", "RestApiId": { "Ref": "ServerlessRestApi" }, @@ -259,7 +259,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "1.6", "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentaa32438b68" + "Ref": "ServerlessRestApiDeployment56105d1476" }, "RestApiId": { "Ref": "ServerlessRestApi" diff --git a/tests/translator/output/intrinsic_functions.json b/tests/translator/output/intrinsic_functions.json index f4eee1a16e..34bfb7a0cc 100644 --- a/tests/translator/output/intrinsic_functions.json +++ b/tests/translator/output/intrinsic_functions.json @@ -243,9 +243,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "MyExplicitApiDeployment7145dd00ce": { + "MyExplicitApiDeployment011ab6fdbf": { "Properties": { - "Description": "RestApi deployment id: 7145dd00cea59b4a62b4d7855add490c587f3f62", + "Description": "RestApi deployment id: 011ab6fdbfec2a0b749690c4c389b4f44e51a053", "RestApiId": { "Ref": "MyExplicitApi" }, @@ -256,7 +256,7 @@ "MyExplicitApidevStage": { "Properties": { "DeploymentId": { - "Ref": "MyExplicitApiDeployment7145dd00ce" + "Ref": "MyExplicitApiDeployment011ab6fdbf" }, "RestApiId": { "Ref": "MyExplicitApi" diff --git a/tests/translator/test_api_resource.py b/tests/translator/test_api_resource.py index 0b13409679..4a086532c1 100644 --- a/tests/translator/test_api_resource.py +++ b/tests/translator/test_api_resource.py @@ -112,6 +112,7 @@ def test_make_auto_deployable_with_swagger_dict(self, LogicalIdGeneratorMock): prefix = "prefix" generator_mock = LogicalIdGeneratorMock.return_value stage = MagicMock() + stage.Variables = None id_val = "SomeLogicalId" full_hash = "127e3fb91142ab1ddc5f5446adb094442581a90d" generator_mock.gen.return_value = id_val @@ -129,6 +130,52 @@ def test_make_auto_deployable_with_swagger_dict(self, LogicalIdGeneratorMock): generator_mock.get_hash.assert_called_once_with(length=40) # getting full SHA stage.update_deployment_ref.assert_called_once_with(id_val) + @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator") + def test_make_auto_deployable_with_stage_variables(self, LogicalIdGeneratorMock): + prefix = "prefix" + generator_mock = LogicalIdGeneratorMock.return_value + stage = MagicMock() + stage.Variables = {"stageVar": "value"} + id_val = "SomeLogicalId" + full_hash = "127e3fb91142ab1ddc5f5446adb094442581a90d" + generator_mock.gen.return_value = id_val + generator_mock.get_hash.return_value = full_hash + + swagger = {"a": "b"} + deployment = ApiGatewayDeployment(logical_id=prefix) + deployment.make_auto_deployable(stage, swagger=swagger) + + LogicalIdGeneratorMock.assert_called_once_with( + prefix, str(swagger) + ApiGatewayDeployment._X_HASH_DELIMITER + json.dumps(stage.Variables, sort_keys=True) + ) + + def test_make_auto_deployable_stage_variables_change_deployment_id(self): + swagger = {"a": "b"} + + def deployment_id_for(variables): + stage = MagicMock() + stage.Variables = variables + deployment = ApiGatewayDeployment(logical_id="prefix") + deployment.make_auto_deployable(stage, swagger=swagger) + return deployment.logical_id + + # Only the stage variables differ, so the deployment must not be reused: reusing it means + # the variable change is never deployed, and UpdateStage resets the active deployment. + self.assertNotEqual(deployment_id_for({"stageVar": "one"}), deployment_id_for({"stageVar": "two"})) + + def test_make_auto_deployable_without_stage_variables_is_unchanged(self): + swagger = {"a": "b"} + + def deployment_id_for(variables): + stage = MagicMock() + stage.Variables = variables + deployment = ApiGatewayDeployment(logical_id="prefix") + deployment.make_auto_deployable(stage, swagger=swagger) + return deployment.logical_id + + # Templates that set no stage variables must keep their existing deployment logical id. + self.assertEqual(deployment_id_for(None), deployment_id_for({})) + @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator") def test_make_auto_deployable_no_swagger(self, LogicalIdGeneratorMock): prefix = "prefix" From a8fee86eeb2751fd818e5c12d21d714d51fd555b Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Fri, 18 Sep 2026 06:21:30 +0000 Subject: [PATCH 2/2] fix: resolve parameter refs when hashing API stage variables The deployment hash used the raw stage variables, so a parameter-driven variable such as {"Ref": "EndpointUri"} hashed identically for every parameter value. Deploying a new value produced the same deployment logical id, leaving the reported failure in place for that case: the variable was never deployed and UpdateStage pointed the stage back at the deployment SAM already knew about. Resolve the variables for hashing only. resolve_parameter_refs mutates its argument and inlines values, and the emitted AWS::ApiGateway::Stage must keep the customer's intrinsics, so a deep copy is resolved and passed to make_auto_deployable rather than assigned back to self.Variables the way Domain is. Adds an end-to-end test asserting two parameter values yield different deployment ids (and that the same value stays stable), plus a guard that the emitted Stage still carries the unresolved Ref. Updates the GetHtmlApi deployment id in the six explicit_api fixtures, whose input template already sets a parameter-driven stage variable. --- samtranslator/model/api/api_generator.py | 6 ++ samtranslator/model/apigateway.py | 11 ++- samtranslator/model/sam_resources.py | 9 +++ .../output/aws-cn/explicit_api.json | 6 +- .../output/aws-cn/explicit_api_openapi_3.json | 6 +- .../output/aws-us-gov/explicit_api.json | 6 +- .../aws-us-gov/explicit_api_openapi_3.json | 6 +- tests/translator/output/explicit_api.json | 6 +- .../output/explicit_api_openapi_3.json | 6 +- tests/translator/test_api_resource.py | 78 +++++++++++++++++++ 10 files changed, 120 insertions(+), 20 deletions(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 260437b41c..620ce7bcc3 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -223,6 +223,7 @@ def __init__( # noqa: PLR0913 policy: Union[dict[str, Any], Intrinsicable[str]] | None = None, security_policy: Intrinsicable[str] | None = None, endpoint_access_mode: Intrinsicable[str] | None = None, + resolved_variables: dict[str, Any] | None = None, ): """Constructs an API Generator class that generates API Gateway resources @@ -283,6 +284,10 @@ def __init__( # noqa: PLR0913 self.policy = policy self.security_policy = security_policy self.endpoint_access_mode = endpoint_access_mode + # Same stage variables as `variables`, but with parameter references resolved. Used only to hash the + # deployment logical id; the stage itself keeps `variables` so the emitted template preserves the + # customer's intrinsics. Falls back to `variables` when the caller did not resolve them. + self.resolved_variables = resolved_variables if resolved_variables is not None else variables def _construct_rest_api(self) -> ApiGatewayRestApi: # noqa: PLR0912 """Constructs and returns the ApiGateway RestApi. @@ -474,6 +479,7 @@ def _construct_stage( self.domain, redeploy_restapi_parameters, self.always_deploy, + stage_variables=self.resolved_variables, ) if self.tags is not None: diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 6fa10b5827..11affe37bf 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -98,7 +98,7 @@ class ApiGatewayDeployment(Resource): runtime_attrs = {"deployment_id": lambda self: ref(self.logical_id)} - def make_auto_deployable( + def make_auto_deployable( # noqa: PLR0913 self, stage: ApiGatewayStage, openapi_version: Union[dict[str, Any], str] | None = None, @@ -106,6 +106,8 @@ def make_auto_deployable( domain: dict[str, Any] | None = None, redeploy_restapi_parameters: Any | None = None, always_deploy: bool | None = False, + *, + stage_variables: dict[str, Any] | None = None, ) -> None: """ Sets up the resource such that it will trigger a re-deployment when Swagger changes or always_deploy is true @@ -116,6 +118,9 @@ def make_auto_deployable( :param openapi_version: string containing value of OpenApiVersion flag in the template :param domain: Dictionary containing the custom domain configuration for the API :param redeploy_restapi_parameters: Dictionary containing the properties for which rest api will be redeployed + :param stage_variables: Stage variables with parameter references already resolved, for hashing only. Callers + must resolve them, because an unresolved {"Ref": "SomeParameter"} hashes identically for every parameter + value. Falls back to the stage's own (possibly unresolved) variables when not supplied. """ if not swagger: return @@ -136,7 +141,9 @@ def make_auto_deployable( # change reuses the existing deployment: the new variables are never deployed, and any # deployment made outside SAM since the last SAM deployment is reverted. # Only added when set, so templates without stage variables keep their existing hash. - stage_variables = getattr(stage, "Variables", None) + # `stage_variables` is the caller-resolved form; the stage's own value is the fallback and may + # still hold intrinsics, which hash identically across parameter values. + stage_variables = stage_variables if stage_variables is not None else getattr(stage, "Variables", None) if stage_variables: hash_input.append(json.dumps(stage_variables, sort_keys=True)) function_names = redeploy_restapi_parameters.get("function_names") if redeploy_restapi_parameters else None diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 09145282db..e7c0a1987e 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1844,6 +1844,14 @@ def to_cloudformation(self, **kwargs) -> list[Resource]: # type: ignore[no-unty self.BinaryMediaTypes = intrinsics_resolver.resolve_parameter_refs(self.BinaryMediaTypes) self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain) self.Auth = intrinsics_resolver.resolve_parameter_refs(self.Auth) + # The deployment logical id is hashed from the stage variables, so parameter references have to be + # resolved for that hash to change when the deployed value changes: an unresolved {"Ref": "SomeParameter"} + # is byte-identical for every parameter value, which would leave the variables-only change undeployed + # (see ApiGatewayDeployment.make_auto_deployable). Unlike Domain above, this is deliberately NOT assigned + # back to self.Variables: resolve_parameter_refs mutates its argument and inlines the values, and the + # emitted AWS::ApiGateway::Stage must keep the customer's intrinsics. Hence a deep copy, used for the + # hash only. + resolved_variables = intrinsics_resolver.resolve_parameter_refs(copy.deepcopy(self.Variables)) redeploy_restapi_parameters = kwargs.get("redeploy_restapi_parameters") shared_api_usage_plan = kwargs.get("shared_api_usage_plan") template_conditions = kwargs.get("conditions") @@ -1889,6 +1897,7 @@ def to_cloudformation(self, **kwargs) -> list[Resource]: # type: ignore[no-unty policy=self.Policy, security_policy=self.SecurityPolicy, endpoint_access_mode=self.EndpointAccessMode, + resolved_variables=resolved_variables, ) generated_resources = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups) diff --git a/tests/translator/output/aws-cn/explicit_api.json b/tests/translator/output/aws-cn/explicit_api.json index 6118e3c579..e9c738301e 100644 --- a/tests/translator/output/aws-cn/explicit_api.json +++ b/tests/translator/output/aws-cn/explicit_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-cn/explicit_api_openapi_3.json b/tests/translator/output/aws-cn/explicit_api_openapi_3.json index 6a2663a4fc..65671de7f1 100644 --- a/tests/translator/output/aws-cn/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-cn/explicit_api_openapi_3.json @@ -68,9 +68,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-us-gov/explicit_api.json b/tests/translator/output/aws-us-gov/explicit_api.json index 35836df5a8..0b804b909b 100644 --- a/tests/translator/output/aws-us-gov/explicit_api.json +++ b/tests/translator/output/aws-us-gov/explicit_api.json @@ -69,9 +69,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json index f53d697d9c..db090550a8 100644 --- a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json @@ -68,9 +68,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/explicit_api.json b/tests/translator/output/explicit_api.json index cbc7dc9d1f..fb03c8695c 100644 --- a/tests/translator/output/explicit_api.json +++ b/tests/translator/output/explicit_api.json @@ -53,9 +53,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -66,7 +66,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/output/explicit_api_openapi_3.json b/tests/translator/output/explicit_api_openapi_3.json index 99e84cfefa..09263bcdb1 100644 --- a/tests/translator/output/explicit_api_openapi_3.json +++ b/tests/translator/output/explicit_api_openapi_3.json @@ -52,9 +52,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeployment03f2a64f84": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: 03f2a64f84966e7db9bc90b90fda3247d9a394d2", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -65,7 +65,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeployment03f2a64f84" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "RestApiId": { "Ref": "GetHtmlApi" diff --git a/tests/translator/test_api_resource.py b/tests/translator/test_api_resource.py index 4a086532c1..51a6c12725 100644 --- a/tests/translator/test_api_resource.py +++ b/tests/translator/test_api_resource.py @@ -106,6 +106,84 @@ def translate_and_find_deployment_ids(manifest): return deployment_ids +@patch("boto3.session.Session.region_name", "ap-southeast-1") +def translate_with_parameter_values(manifest, extra_parameter_values): + """Transform `manifest` with the shared parameter values plus `extra_parameter_values`. + + Region resolution is pinned rather than inherited: without this, partition lookup falls through to the + ambient boto3 session and the test fails with NoRegionFound on a machine that has no region configured. + """ + parameter_values = {**get_template_parameter_values(), **extra_parameter_values} + with patch("samtranslator.translator.arn_generator._get_region_from_session") as mock_region: + mock_region.return_value = "ap-southeast-1" + return transform(manifest, parameter_values, mock_policy_loader) + + +def find_deployment_ids(output_fragment): + return { + key for key, value in output_fragment["Resources"].items() if value["Type"] == "AWS::ApiGateway::Deployment" + } + + +def find_stage_variables(output_fragment): + return [ + value["Properties"].get("Variables") + for value in output_fragment["Resources"].values() + if value["Type"] == "AWS::ApiGateway::Stage" + ] + + +def _parameter_driven_stage_variable_manifest(): + return { + "Transform": "AWS::Serverless-2016-10-31", + "Parameters": {"EndpointUri": {"Type": "String"}}, + "Resources": { + "ExplicitApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "prod", + "DefinitionUri": "s3://mybucket/swagger.json?versionId=123", + "Variables": {"EndpointUri": {"Ref": "EndpointUri"}}, + }, + } + }, + } + + +@patch("botocore.client.ClientEndpointBridge._check_default_region", mock_get_region) +def test_redeploy_when_parameter_driven_stage_variable_changes(): + """A stage variable driven by a template parameter must redeploy when the parameter's value changes. + + Hashing the unresolved {"Ref": "EndpointUri"} yields the same deployment logical id for every parameter + value, so the new variable would never be deployed and UpdateStage would point the stage back at the + deployment SAM already knows about -- the failure reported in #3703, just via a parameter. + """ + manifest = _parameter_driven_stage_variable_manifest() + + first = find_deployment_ids(translate_with_parameter_values(manifest, {"EndpointUri": "https://one.example.com"})) + second = find_deployment_ids(translate_with_parameter_values(manifest, {"EndpointUri": "https://two.example.com"})) + + assert first != second + + # Same parameter value must stay stable, so an unchanged stack does not churn its deployment. + third = find_deployment_ids(translate_with_parameter_values(manifest, {"EndpointUri": "https://one.example.com"})) + assert first == third + + +@patch("botocore.client.ClientEndpointBridge._check_default_region", mock_get_region) +def test_parameter_driven_stage_variables_are_not_inlined_into_stage(): + """Resolving variables for the hash must not leak resolved values into the emitted template. + + resolve_parameter_refs mutates its argument, so hashing a resolved value has to work on a copy; otherwise + the AWS::ApiGateway::Stage would emit the parameter's value instead of the customer's Ref. + """ + manifest = _parameter_driven_stage_variable_manifest() + + output_fragment = translate_with_parameter_values(manifest, {"EndpointUri": "https://one.example.com"}) + + assert find_stage_variables(output_fragment) == [{"EndpointUri": {"Ref": "EndpointUri"}}] + + class TestApiGatewayDeploymentResource(TestCase): @patch("samtranslator.translator.logical_id_generator.LogicalIdGenerator") def test_make_auto_deployable_with_swagger_dict(self, LogicalIdGeneratorMock):