diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 260437b41..620ce7bcc 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 5d9d44703..11affe37b 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 @@ -131,6 +136,16 @@ 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` 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 # The deployment logical id is + "Deployment" # The keyword "Deployment" is removed and all the function names associated with api is obtained diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 09145282d..e7c0a1987 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/api_with_openapi_definition_body_no_flag.json b/tests/translator/output/api_with_openapi_definition_body_no_flag.json index b7904447c..c6e9e62b7 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 d215e100d..73d5ccb49 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 ca6140b3f..6f4b1f3cb 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 246a43aef..92be58e11 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 63eefda8a..e9c738301 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": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "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 36f676857..65671de7f 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": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "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 b61186ff1..45c4830d4 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 c52903753..180c9aa90 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 ee86381d3..eff4c0773 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 7a93d4676..b91054164 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 d95ca5396..c3086b175 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 103de4247..3648f6d17 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 14ff0657f..0b804b909 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": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -82,7 +82,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "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 5383290e6..db090550a 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": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -81,7 +81,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "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 35a2609b2..585ac2cd1 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 633d47f15..82127cdf2 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 0f1844e2c..4edd571c5 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 b2b44466b..e5159d8af 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 95c019102..fb03c8695 100644 --- a/tests/translator/output/explicit_api.json +++ b/tests/translator/output/explicit_api.json @@ -53,9 +53,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "GetHtmlApiDeploymentf117c932f7": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -66,7 +66,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "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 5aac47a16..09263bcdb 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": { + "GetHtmlApiDeploymentb28d6f4807": { "Properties": { - "Description": "RestApi deployment id: f117c932f75cfa87d23dfed64e9430d0081ef289", + "Description": "RestApi deployment id: b28d6f4807cb961a23c29eeed9b708cab7c109f0", "RestApiId": { "Ref": "GetHtmlApi" }, @@ -65,7 +65,7 @@ "GetHtmlApiStage": { "Properties": { "DeploymentId": { - "Ref": "GetHtmlApiDeploymentf117c932f7" + "Ref": "GetHtmlApiDeploymentb28d6f4807" }, "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 574336b38..792b00b41 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 1eed92346..406eeebe8 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 6c624d7f4..5ed252057 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 f4eee1a16..34bfb7a0c 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 0b1340967..51a6c1272 100644 --- a/tests/translator/test_api_resource.py +++ b/tests/translator/test_api_resource.py @@ -106,12 +106,91 @@ 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): 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 +208,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"