From 4125b0183d5d450959290821d674b9903a1e50da Mon Sep 17 00:00:00 2001 From: "localstack[bot]" Date: Thu, 27 Mar 2025 09:10:17 +0000 Subject: [PATCH 001/108] prepare next development iteration From 90ccf60d5e6394125c2d0f5aac7ca3d1a3fc25fa Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Thu, 27 Mar 2025 19:09:58 +0530 Subject: [PATCH 002/108] add localstack 4.3 blog to the README (#12445) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 856d337effd5e..23c071c33d9d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-:zap: We are thrilled to announce the release of LocalStack 4.1 :zap: +:zap: We are thrilled to announce the release of LocalStack 4.3 :zap:

@@ -93,7 +93,7 @@ Start LocalStack inside a Docker container by running: / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,< /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_| -- LocalStack CLI: 4.1.0 +- LocalStack CLI: 4.3.0 - Profile: default - App: https://app.localstack.cloud From ccddefd5285bcf95ec202ab63a0e2e9d09f5efea Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Fri, 28 Mar 2025 16:48:21 +0100 Subject: [PATCH 003/108] Fix flaky lambda test event retry reserved concurrency (#12441) --- .../localstack/testing/pytest/fixtures.py | 32 +++++ .../lambda_/functions/lambda_notifier.py | 40 +++++++ tests/aws/services/lambda_/test_lambda.py | 112 +++++++++++++----- .../lambda_/test_lambda.snapshot.json | 4 +- .../lambda_/test_lambda.validation.json | 2 +- 5 files changed, 157 insertions(+), 33 deletions(-) create mode 100644 tests/aws/services/lambda_/functions/lambda_notifier.py diff --git a/localstack-core/localstack/testing/pytest/fixtures.py b/localstack-core/localstack/testing/pytest/fixtures.py index d526097aef1cb..66cc5c2f016eb 100644 --- a/localstack-core/localstack/testing/pytest/fixtures.py +++ b/localstack-core/localstack/testing/pytest/fixtures.py @@ -67,6 +67,38 @@ from mypy_boto3_sqs.type_defs import MessageTypeDef +@pytest.fixture(scope="session") +def aws_client_no_retry(aws_client_factory): + """ + This fixture can be used to obtain Boto clients with disabled retries for testing. + botocore docs: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#configuring-a-retry-mode + + Use this client when testing exceptions (i.e., with pytest.raises(...)) or expected errors (e.g., status code 500) + to avoid unnecessary retries and mitigate test flakiness if the tested error condition is time-bound. + + This client is needed for the following errors, exceptions, and HTTP status codes defined by the legacy retry mode: + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode + General socket/connection errors: + * ConnectionError + * ConnectionClosedError + * ReadTimeoutError + * EndpointConnectionError + + Service-side throttling/limit errors and exceptions: + * Throttling + * ThrottlingException + * ThrottledException + * RequestThrottledException + * ProvisionedThroughputExceededException + + HTTP status codes: 429, 500, 502, 503, 504, and 509 + + Hence, this client is not needed for a `ResourceNotFound` error (but it doesn't harm). + """ + no_retry_config = botocore.config.Config(retries={"max_attempts": 1}) + return aws_client_factory(config=no_retry_config) + + @pytest.fixture(scope="class") def aws_http_client_factory(aws_session): """ diff --git a/tests/aws/services/lambda_/functions/lambda_notifier.py b/tests/aws/services/lambda_/functions/lambda_notifier.py new file mode 100644 index 0000000000000..01b75c6fd64b9 --- /dev/null +++ b/tests/aws/services/lambda_/functions/lambda_notifier.py @@ -0,0 +1,40 @@ +import datetime +import json +import os +import time + +import boto3 + +sqs_client = boto3.client("sqs", endpoint_url=os.environ.get("AWS_ENDPOINT_URL")) + + +def handler(event, context): + """Example: Send a message to the queue_url provided in notify and then wait for 7 seconds. + The message includes the value of the environment variable called "FUNCTION_VARIANT". + aws_client.lambda_.invoke( + FunctionName=fn_arn, + InvocationType="Event", + Payload=json.dumps({"notify": queue_url, "env_var": "FUNCTION_VARIANT", "label": "01-sleep", "wait": 7}) + ) + + Parameters: + * `notify`: SQS queue URL to notify a message + * `env_var`: Name of the environment variable that should be included in the message + * `label`: Label to be included in the message + * `wait`: Time in seconds to sleep + """ + if queue_url := event.get("notify"): + message = { + "request_id": context.aws_request_id, + "timestamp": datetime.datetime.now(datetime.UTC).isoformat(), + } + if env_var := event.get("env_var"): + message[env_var] = os.environ[env_var] + if label := event.get("label"): + message["label"] = label + print(f"Notify message: {message}") + sqs_client.send_message(QueueUrl=queue_url, MessageBody=json.dumps(message)) + + if wait_time := event.get("wait"): + print(f"Sleeping for {wait_time} seconds ...") + time.sleep(wait_time) diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index 11b754d296fe1..cba8061226134 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -30,7 +30,10 @@ get_invoke_init_type, update_done, ) -from localstack.testing.aws.util import create_client_with_keys, is_aws_cloud +from localstack.testing.aws.util import ( + create_client_with_keys, + is_aws_cloud, +) from localstack.testing.pytest import markers from localstack.testing.snapshots.transformer_utility import PATTERN_UUID from localstack.utils import files, platform, testutil @@ -123,6 +126,7 @@ TEST_LAMBDA_PYTHON_MULTIPLE_HANDLERS = os.path.join( THIS_FOLDER, "functions/lambda_multiple_handlers.py" ) +TEST_LAMBDA_NOTIFIER = os.path.join(THIS_FOLDER, "functions/lambda_notifier.py") PYTHON_TEST_RUNTIMES = RUNTIMES_AGGREGATED["python"] NODE_TEST_RUNTIMES = RUNTIMES_AGGREGATED["nodejs"] @@ -2614,18 +2618,37 @@ def _invoke_lambda(): assert not errored @markers.aws.validated - @pytest.mark.skip(reason="flaky") - def test_reserved_concurrency_async_queue(self, create_lambda_function, snapshot, aws_client): + def test_reserved_concurrency_async_queue( + self, + create_lambda_function, + sqs_create_queue, + sqs_collect_messages, + snapshot, + aws_client, + aws_client_no_retry, + ): + """Test async/event invoke retry behavior due to limited reserved concurrency. + Timeline: + 1) Set ReservedConcurrentExecutions=1 + 2) sync_invoke_warm_up => ok + 3) async_invoke_one => ok + 4) async_invoke_two => gets retried + 5) sync invoke => fails with TooManyRequestsException + 6) Set ReservedConcurrentExecutions=3 + 7) sync_invoke_final => ok + """ min_concurrent_executions = 10 + 3 check_concurrency_quota(aws_client, min_concurrent_executions) + queue_url = sqs_create_queue() + func_name = f"test_lambda_{short_uid()}" create_lambda_function( func_name=func_name, - handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, + handler_file=TEST_LAMBDA_NOTIFIER, runtime=Runtime.python3_12, client=aws_client.lambda_, - timeout=20, + timeout=30, ) fn = aws_client.lambda_.get_function_configuration( @@ -2641,24 +2664,46 @@ def test_reserved_concurrency_async_queue(self, create_lambda_function, snapshot snapshot.match("put_fn_concurrency", put_fn_concurrency) # warm up the Lambda function to mitigate flakiness due to cold start - aws_client.lambda_.invoke(FunctionName=fn_arn, InvocationType="RequestResponse") + sync_invoke_warm_up = aws_client.lambda_.invoke( + FunctionName=fn_arn, InvocationType="RequestResponse" + ) + assert "FunctionError" not in sync_invoke_warm_up - # simultaneously queue two event invocations - # The first event invoke gets executed immediately - aws_client.lambda_.invoke( - FunctionName=fn_arn, InvocationType="Event", Payload=json.dumps({"wait": 15}) + # Immediately queue two event invocations: + # 1) The first event invoke gets executed immediately + async_invoke_one = aws_client.lambda_.invoke( + FunctionName=fn_arn, + InvocationType="Event", + Payload=json.dumps({"notify": queue_url, "wait": 15}), ) - # The second event invoke gets throttled and re-scheduled with an internal retry - aws_client.lambda_.invoke( - FunctionName=fn_arn, InvocationType="Event", Payload=json.dumps({"wait": 10}) + assert "FunctionError" not in async_invoke_one + # 2) The second event invoke gets throttled and re-scheduled with an internal retry + async_invoke_two = aws_client.lambda_.invoke( + FunctionName=fn_arn, + InvocationType="Event", + Payload=json.dumps({"notify": queue_url}), ) + assert "FunctionError" not in async_invoke_two - # Ensure one event invocation is being executed and the other one is in the queue. - time.sleep(5) + # Wait until the first async invoke is being executed while the second async invoke is in the queue. + messages = sqs_collect_messages( + queue_url, + expected=1, + timeout=15, + ) + async_invoke_one_notification = json.loads(messages[0]["Body"]) + assert ( + async_invoke_one_notification["request_id"] + == async_invoke_one["ResponseMetadata"]["RequestId"] + ) # Synchronous invocations raise an exception because insufficient reserved concurrency is available + # It is important to disable botocore retries because the concurrency limit is time-bound because it only + # triggers as long as the first async invoke is processing! with pytest.raises(aws_client.lambda_.exceptions.TooManyRequestsException) as e: - aws_client.lambda_.invoke(FunctionName=fn_arn, InvocationType="RequestResponse") + aws_client_no_retry.lambda_.invoke( + FunctionName=fn_arn, InvocationType="RequestResponse" + ) snapshot.match("too_many_requests_exc", e.value.response) # ReservedConcurrentExecutions=2 is insufficient because the throttled async event invoke might be @@ -2666,21 +2711,28 @@ def test_reserved_concurrency_async_queue(self, create_lambda_function, snapshot aws_client.lambda_.put_function_concurrency( FunctionName=func_name, ReservedConcurrentExecutions=3 ) - aws_client.lambda_.invoke(FunctionName=fn_arn, InvocationType="RequestResponse") - - def assert_events(): - log_events = aws_client.logs.filter_log_events( - logGroupName=f"/aws/lambda/{func_name}", - )["events"] - invocation_count = len( - [event["message"] for event in log_events if event["message"].startswith("REPORT")] - ) - assert invocation_count == 4 - - retry(assert_events, retries=120, sleep=2) + # Invocations succeed after raising reserved concurrency + sync_invoke_final = aws_client.lambda_.invoke( + FunctionName=fn_arn, + InvocationType="RequestResponse", + Payload=json.dumps({"notify": queue_url}), + ) + assert "FunctionError" not in sync_invoke_final - # TODO: snapshot logs & request ID for correlation after request id gets propagated - # https://github.com/localstack/localstack/pull/7874 + # Contains the re-queued `async_invoke_two` and the `sync_invoke_final`, but the order might differ + # depending on whether invoke_two gets re-schedule before or after the final invoke. + # AWS docs: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-error-handling.html + # "The retry interval increases exponentially from 1 second after the first attempt to a maximum of 5 minutes." + final_messages = sqs_collect_messages( + queue_url, + expected=2, + timeout=20, + ) + invoked_request_ids = {json.loads(msg["Body"])["request_id"] for msg in final_messages} + assert { + async_invoke_two["ResponseMetadata"]["RequestId"], + sync_invoke_final["ResponseMetadata"]["RequestId"], + } == invoked_request_ids @markers.snapshot.skip_snapshot_verify(paths=["$..Attributes.AWSTraceHeader"]) @markers.aws.validated diff --git a/tests/aws/services/lambda_/test_lambda.snapshot.json b/tests/aws/services/lambda_/test_lambda.snapshot.json index cb24e3154abd6..85733509934a2 100644 --- a/tests/aws/services/lambda_/test_lambda.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda.snapshot.json @@ -2982,7 +2982,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency_async_queue": { - "recorded-date": "08-04-2024, 17:07:59", + "recorded-date": "26-03-2025, 10:53:54", "recorded-content": { "fn": { "Architectures": [ @@ -3019,7 +3019,7 @@ "OptimizationStatus": "Off" }, "State": "Active", - "Timeout": 20, + "Timeout": 30, "TracingConfig": { "Mode": "PassThrough" }, diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json index 49d07c303273f..adc3a699f0367 100644 --- a/tests/aws/services/lambda_/test_lambda.validation.json +++ b/tests/aws/services/lambda_/test_lambda.validation.json @@ -78,7 +78,7 @@ "last_validated_date": "2024-04-08T17:08:10+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency_async_queue": { - "last_validated_date": "2024-04-08T17:07:56+00:00" + "last_validated_date": "2025-03-26T10:54:29+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_provisioned_overlap": { "last_validated_date": "2024-04-08T17:10:36+00:00" From e12aa89fa4b9233a0f4f5d8eba1176ac60424014 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:07:19 +0200 Subject: [PATCH 004/108] Update ASF APIs, update ssm provider signature (#12453) Co-authored-by: LocalStack Bot Co-authored-by: Alexander Rashed --- .../localstack/aws/api/apigateway/__init__.py | 6 +++++ .../aws/api/cloudformation/__init__.py | 27 ++++++++++++++++++- .../localstack/aws/api/ssm/__init__.py | 16 +++++++++++ .../localstack/services/ssm/provider.py | 2 ++ pyproject.toml | 4 +-- requirements-base-runtime.txt | 4 +-- requirements-dev.txt | 6 ++--- requirements-runtime.txt | 6 ++--- requirements-test.txt | 6 ++--- requirements-typehint.txt | 6 ++--- 10 files changed, 66 insertions(+), 17 deletions(-) diff --git a/localstack-core/localstack/aws/api/apigateway/__init__.py b/localstack-core/localstack/aws/api/apigateway/__init__.py index 47bd84435db2f..f8a46b7b5e4c6 100644 --- a/localstack-core/localstack/aws/api/apigateway/__init__.py +++ b/localstack-core/localstack/aws/api/apigateway/__init__.py @@ -124,6 +124,11 @@ class IntegrationType(StrEnum): AWS_PROXY = "AWS_PROXY" +class IpAddressType(StrEnum): + ipv4 = "ipv4" + dualstack = "dualstack" + + class LocationStatusType(StrEnum): DOCUMENTED = "DOCUMENTED" UNDOCUMENTED = "UNDOCUMENTED" @@ -449,6 +454,7 @@ class MutualTlsAuthenticationInput(TypedDict, total=False): class EndpointConfiguration(TypedDict, total=False): types: Optional[ListOfEndpointType] + ipAddressType: Optional[IpAddressType] vpcEndpointIds: Optional[ListOfString] diff --git a/localstack-core/localstack/aws/api/cloudformation/__init__.py b/localstack-core/localstack/aws/api/cloudformation/__init__.py index 32951575e960c..fcd83677aac19 100644 --- a/localstack-core/localstack/aws/api/cloudformation/__init__.py +++ b/localstack-core/localstack/aws/api/cloudformation/__init__.py @@ -120,6 +120,7 @@ ResourceStatusReason = str ResourceToSkip = str ResourceType = str +ResourceTypeFilter = str ResourceTypePrefix = str ResourcesFailed = int ResourcesPending = int @@ -526,6 +527,11 @@ class ResourceStatus(StrEnum): ROLLBACK_FAILED = "ROLLBACK_FAILED" +class ScanType(StrEnum): + FULL = "FULL" + PARTIAL = "PARTIAL" + + class StackDriftDetectionStatus(StrEnum): DETECTION_IN_PROGRESS = "DETECTION_IN_PROGRESS" DETECTION_FAILED = "DETECTION_FAILED" @@ -1536,6 +1542,16 @@ class DescribeResourceScanInput(ServiceRequest): ResourceScanId: ResourceScanId +ResourceTypeFilters = List[ResourceTypeFilter] + + +class ScanFilter(TypedDict, total=False): + Types: Optional[ResourceTypeFilters] + + +ScanFilters = List[ScanFilter] + + class DescribeResourceScanOutput(TypedDict, total=False): ResourceScanId: Optional[ResourceScanId] Status: Optional[ResourceScanStatus] @@ -1546,6 +1562,7 @@ class DescribeResourceScanOutput(TypedDict, total=False): ResourceTypes: Optional[ResourceTypes] ResourcesScanned: Optional[ResourcesScanned] ResourcesRead: Optional[ResourcesRead] + ScanFilters: Optional[ScanFilters] class DescribeStackDriftDetectionStatusInput(ServiceRequest): @@ -2246,6 +2263,7 @@ class ListResourceScanResourcesOutput(TypedDict, total=False): class ListResourceScansInput(ServiceRequest): NextToken: Optional[NextToken] MaxResults: Optional[ResourceScannerMaxResults] + ScanTypeFilter: Optional[ScanType] class ResourceScanSummary(TypedDict, total=False): @@ -2255,6 +2273,7 @@ class ResourceScanSummary(TypedDict, total=False): StartTime: Optional[Timestamp] EndTime: Optional[Timestamp] PercentageCompleted: Optional[PercentageCompleted] + ScanType: Optional[ScanType] ResourceScanSummaries = List[ResourceScanSummary] @@ -2745,6 +2764,7 @@ class SignalResourceInput(ServiceRequest): class StartResourceScanInput(ServiceRequest): ClientRequestToken: Optional[ClientRequestToken] + ScanFilters: Optional[ScanFilters] class StartResourceScanOutput(TypedDict, total=False): @@ -3482,6 +3502,7 @@ def list_resource_scans( context: RequestContext, next_token: NextToken = None, max_results: ResourceScannerMaxResults = None, + scan_type_filter: ScanType = None, **kwargs, ) -> ListResourceScansOutput: raise NotImplementedError @@ -3709,7 +3730,11 @@ def signal_resource( @handler("StartResourceScan") def start_resource_scan( - self, context: RequestContext, client_request_token: ClientRequestToken = None, **kwargs + self, + context: RequestContext, + client_request_token: ClientRequestToken = None, + scan_filters: ScanFilters = None, + **kwargs, ) -> StartResourceScanOutput: raise NotImplementedError diff --git a/localstack-core/localstack/aws/api/ssm/__init__.py b/localstack-core/localstack/aws/api/ssm/__init__.py index a906bb4247944..ec192fa21d5bf 100644 --- a/localstack-core/localstack/aws/api/ssm/__init__.py +++ b/localstack-core/localstack/aws/api/ssm/__init__.py @@ -252,6 +252,7 @@ ParametersFilterValue = str PatchAdvisoryId = str PatchArch = str +PatchAvailableSecurityUpdateCount = int PatchBaselineMaxResults = int PatchBugzillaId = str PatchCVEId = str @@ -960,6 +961,7 @@ class PatchComplianceDataState(StrEnum): MISSING = "MISSING" NOT_APPLICABLE = "NOT_APPLICABLE" FAILED = "FAILED" + AVAILABLE_SECURITY_UPDATE = "AVAILABLE_SECURITY_UPDATE" class PatchComplianceLevel(StrEnum): @@ -971,6 +973,11 @@ class PatchComplianceLevel(StrEnum): UNSPECIFIED = "UNSPECIFIED" +class PatchComplianceStatus(StrEnum): + COMPLIANT = "COMPLIANT" + NON_COMPLIANT = "NON_COMPLIANT" + + class PatchDeploymentStatus(StrEnum): APPROVED = "APPROVED" PENDING_APPROVAL = "PENDING_APPROVAL" @@ -2510,6 +2517,7 @@ class BaselineOverride(TypedDict, total=False): RejectedPatchesAction: Optional[PatchAction] ApprovedPatchesEnableNonSecurity: Optional[Boolean] Sources: Optional[PatchSourceList] + AvailableSecurityUpdatesComplianceStatus: Optional[PatchComplianceStatus] InstanceIdList = List[InstanceId] @@ -2970,6 +2978,7 @@ class CreatePatchBaselineRequest(ServiceRequest): RejectedPatchesAction: Optional[PatchAction] Description: Optional[BaselineDescription] Sources: Optional[PatchSourceList] + AvailableSecurityUpdatesComplianceStatus: Optional[PatchComplianceStatus] ClientToken: Optional[ClientToken] Tags: Optional[TagList] @@ -3547,6 +3556,7 @@ class InstancePatchState(TypedDict, total=False): FailedCount: Optional[PatchFailedCount] UnreportedNotApplicableCount: Optional[PatchUnreportedNotApplicableCount] NotApplicableCount: Optional[PatchNotApplicableCount] + AvailableSecurityUpdateCount: Optional[PatchAvailableSecurityUpdateCount] OperationStartTime: DateTime OperationEndTime: DateTime Operation: PatchOperationType @@ -4089,6 +4099,7 @@ class DescribePatchGroupStateResult(TypedDict, total=False): InstancesWithCriticalNonCompliantPatches: Optional[InstancesCount] InstancesWithSecurityNonCompliantPatches: Optional[InstancesCount] InstancesWithOtherNonCompliantPatches: Optional[InstancesCount] + InstancesWithAvailableSecurityUpdates: Optional[Integer] class DescribePatchGroupsRequest(ServiceRequest): @@ -4857,6 +4868,7 @@ class GetPatchBaselineResult(TypedDict, total=False): ModifiedDate: Optional[DateTime] Description: Optional[BaselineDescription] Sources: Optional[PatchSourceList] + AvailableSecurityUpdatesComplianceStatus: Optional[PatchComplianceStatus] class GetResourcePoliciesRequest(ServiceRequest): @@ -5835,6 +5847,7 @@ class UpdatePatchBaselineRequest(ServiceRequest): RejectedPatchesAction: Optional[PatchAction] Description: Optional[BaselineDescription] Sources: Optional[PatchSourceList] + AvailableSecurityUpdatesComplianceStatus: Optional[PatchComplianceStatus] Replace: Optional[Boolean] @@ -5853,6 +5866,7 @@ class UpdatePatchBaselineResult(TypedDict, total=False): ModifiedDate: Optional[DateTime] Description: Optional[BaselineDescription] Sources: Optional[PatchSourceList] + AvailableSecurityUpdatesComplianceStatus: Optional[PatchComplianceStatus] class UpdateResourceDataSyncRequest(ServiceRequest): @@ -6055,6 +6069,7 @@ def create_patch_baseline( rejected_patches_action: PatchAction = None, description: BaselineDescription = None, sources: PatchSourceList = None, + available_security_updates_compliance_status: PatchComplianceStatus = None, client_token: ClientToken = None, tags: TagList = None, **kwargs, @@ -7522,6 +7537,7 @@ def update_patch_baseline( rejected_patches_action: PatchAction = None, description: BaselineDescription = None, sources: PatchSourceList = None, + available_security_updates_compliance_status: PatchComplianceStatus = None, replace: Boolean = None, **kwargs, ) -> UpdatePatchBaselineResult: diff --git a/localstack-core/localstack/services/ssm/provider.py b/localstack-core/localstack/services/ssm/provider.py index 189455ea258ef..7787daa091383 100644 --- a/localstack-core/localstack/services/ssm/provider.py +++ b/localstack-core/localstack/services/ssm/provider.py @@ -60,6 +60,7 @@ PatchAction, PatchBaselineMaxResults, PatchComplianceLevel, + PatchComplianceStatus, PatchFilterGroup, PatchIdList, PatchOrchestratorFilterList, @@ -201,6 +202,7 @@ def create_patch_baseline( rejected_patches_action: PatchAction = None, description: BaselineDescription = None, sources: PatchSourceList = None, + available_security_updates_compliance_status: PatchComplianceStatus = None, client_token: ClientToken = None, tags: TagList = None, **kwargs, diff --git a/pyproject.toml b/pyproject.toml index a992abc6ef877..c57a71eda550a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.37.18", + "boto3==1.37.23", # pinned / updated by ASF update action - "botocore==1.37.18", + "botocore==1.37.23", "awscrt>=0.13.14", "cbor2>=5.5.0", "dnspython>=1.16.0", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 052fb394ca523..233776ada790e 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -11,9 +11,9 @@ attrs==25.3.0 # referencing awscrt==0.25.4 # via localstack-core (pyproject.toml) -boto3==1.37.18 +boto3==1.37.23 # via localstack-core (pyproject.toml) -botocore==1.37.18 +botocore==1.37.23 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index fef65fabee4bd..2f2e44af43737 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.18 +awscli==1.38.23 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.18 +boto3==1.37.23 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.18 +botocore==1.37.23 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index a75269182aedd..c8f16282c01f7 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -29,17 +29,17 @@ aws-sam-translator==1.95.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.18 +awscli==1.38.23 # via localstack-core (pyproject.toml) awscrt==0.25.4 # via localstack-core -boto3==1.37.18 +boto3==1.37.23 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.18 +botocore==1.37.23 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 703ff7f52941e..ca5b4924d67d4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.18 +awscli==1.38.23 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.18 +boto3==1.37.23 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.18 +botocore==1.37.23 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 61fb83ea593c0..88878aeb13889 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -41,11 +41,11 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.18 +awscli==1.38.23 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.18 +boto3==1.37.23 # via # amazon-kclpy # aws-sam-translator @@ -53,7 +53,7 @@ boto3==1.37.18 # moto-ext boto3-stubs==1.37.19 # via localstack-core (pyproject.toml) -botocore==1.37.18 +botocore==1.37.23 # via # aws-xray-sdk # awscli From c3ee300760b308e9e6caec7b6e6eeecee790803b Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Mon, 31 Mar 2025 12:10:12 +0100 Subject: [PATCH 005/108] CFn: add tests for capturing change set process (#12438) --- .../cloudformation/api/test_changesets.py | 597 +++ .../api/test_changesets.snapshot.json | 4402 +++++++++++++++++ .../api/test_changesets.validation.json | 24 + 3 files changed, 5023 insertions(+) diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index e7d9a793d7704..c03b9e22b8d92 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -1,8 +1,14 @@ +import json import os.path +from collections import defaultdict +from typing import Callable import pytest from botocore.exceptions import ClientError +from localstack import config +from localstack.aws.api.cloudformation import StackEvent +from localstack.aws.connect import ServiceLevelClientFactory from localstack.testing.aws.cloudformation_utils import ( load_template_file, load_template_raw, @@ -10,6 +16,7 @@ ) from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.utils.functions import call_safe from localstack.utils.strings import short_uid from localstack.utils.sync import ShortCircuitWaitException, poll_condition, wait_until from tests.aws.services.cloudformation.api.test_stacks import ( @@ -17,6 +24,10 @@ ) +def is_v2_engine() -> bool: + return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2" + + @markers.aws.validated def test_create_change_set_without_parameters( cleanup_stacks, cleanup_changesets, is_change_set_created_and_available, aws_client @@ -1075,3 +1086,589 @@ def test_describe_change_set_with_similarly_named_stacks(deploy_cfn_template, aw )["ChangeSetId"] == response["Id"] ) + + +PerResourceStackEvents = dict[str, list[StackEvent]] + + +@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +class TestCaptureUpdateProcess: + @pytest.fixture + def capture_per_resource_events( + self, + aws_client: ServiceLevelClientFactory, + ) -> Callable[[str], PerResourceStackEvents]: + def capture(stack_name: str) -> PerResourceStackEvents: + events = aws_client.cloudformation.describe_stack_events(StackName=stack_name)[ + "StackEvents" + ] + per_resource_events = defaultdict(list) + for event in events: + if logical_resource_id := event.get("LogicalResourceId"): + per_resource_events[logical_resource_id].append(event) + return per_resource_events + + return capture + + @pytest.fixture + def capture_update_process(self, aws_client, snapshot, cleanups, capture_per_resource_events): + """ + Fixture to deploy a new stack (via creating and executing a change set), then updating the + stack with a second template (via creating and executing a change set). + """ + + stack_name = f"stack-{short_uid()}" + change_set_name = f"cs-{short_uid()}" + + def inner(t1: dict | str, t2: dict | str, p1: dict | None = None, p2: dict | None = None): + if isinstance(t1, dict): + t1 = json.dumps(t1) + elif isinstance(t1, str): + with open(t1) as infile: + t1 = infile.read() + if isinstance(t2, dict): + t2 = json.dumps(t2) + elif isinstance(t2, str): + with open(t2) as infile: + t2 = infile.read() + + p1 = p1 or {} + p2 = p2 or {} + + # deploy original stack + change_set_details = aws_client.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=t1, + ChangeSetType="CREATE", + Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p1.items()], + ) + snapshot.match("create-change-set-1", change_set_details) + stack_id = change_set_details["StackId"] + change_set_id = change_set_details["Id"] + aws_client.cloudformation.get_waiter("change_set_create_complete").wait( + ChangeSetName=change_set_id + ) + cleanups.append( + lambda: call_safe( + aws_client.cloudformation.delete_change_set, + kwargs=dict(ChangeSetName=change_set_id), + ) + ) + + describe_change_set_with_prop_values = aws_client.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=True + ) + snapshot.match( + "describe-change-set-1-prop-values", describe_change_set_with_prop_values + ) + describe_change_set_without_prop_values = aws_client.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=False + ) + snapshot.match("describe-change-set-1", describe_change_set_without_prop_values) + + execute_results = aws_client.cloudformation.execute_change_set( + ChangeSetName=change_set_id + ) + snapshot.match("execute-change-set-1", execute_results) + aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_id) + + # ensure stack deletion + cleanups.append( + lambda: call_safe( + aws_client.cloudformation.delete_stack, kwargs=dict(StackName=stack_id) + ) + ) + + describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] + snapshot.match("post-create-1-describe", describe) + + # update stack + change_set_details = aws_client.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=t2, + ChangeSetType="UPDATE", + Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p2.items()], + ) + snapshot.match("create-change-set-2", change_set_details) + stack_id = change_set_details["StackId"] + change_set_id = change_set_details["Id"] + aws_client.cloudformation.get_waiter("change_set_create_complete").wait( + ChangeSetName=change_set_id + ) + + describe_change_set_with_prop_values = aws_client.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=True + ) + snapshot.match( + "describe-change-set-2-prop-values", describe_change_set_with_prop_values + ) + describe_change_set_without_prop_values = aws_client.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=False + ) + snapshot.match("describe-change-set-2", describe_change_set_without_prop_values) + + execute_results = aws_client.cloudformation.execute_change_set( + ChangeSetName=change_set_id + ) + snapshot.match("execute-change-set-2", execute_results) + aws_client.cloudformation.get_waiter("stack_update_complete").wait(StackName=stack_id) + + describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] + snapshot.match("post-create-2-describe", describe) + + events = capture_per_resource_events(stack_name) + snapshot.match("per-resource-events", events) + + # delete stack + aws_client.cloudformation.delete_stack(StackName=stack_id) + aws_client.cloudformation.get_waiter("stack_delete_complete").wait(StackName=stack_id) + describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] + snapshot.match("delete-describe", describe) + + yield inner + + @markers.aws.validated + def test_direct_update( + self, + capture_update_process, + ): + """ + Update a stack with a static change (i.e. in the text of the template). + + Conclusions: + - A static change in the template that's not invoking an intrinsic function + (`Ref`, `Fn::GetAtt` etc.) is resolved by the deployment engine synchronously + during the `create_change_set` invocation + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + t1 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + }, + }, + }, + } + t2 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + }, + }, + }, + } + + capture_update_process(t1, t2) + + @markers.aws.validated + def test_dynamic_update( + self, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed statically + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The value of B on creation is "known after apply" even though the resolved + property value is known statically + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + t1 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + t2 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + + capture_update_process(t1, t2) + + @markers.aws.validated + def test_parameter_changes( + self, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via a template parameter + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The value of B on creation is "known after apply" even though the resolved + property value is known statically + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + t1 = { + "Parameters": { + "TopicName": { + "Type": "String", + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": {"Ref": "TopicName"}, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + + capture_update_process(t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) + + @markers.aws.validated + def test_mappings_with_static_fields( + self, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via looking up a static value in a mapping + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - On first deploy the contents of the map is resolved completely + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = "key1" + name2 = "key2" + t1 = { + "Mappings": { + "MyMap": { + "MyKey": { + name1: "MyTopicName", + name2: "MyNewTopicName", + }, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + name1, + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + t2 = { + "Mappings": { + "MyMap": { + "MyKey": { + name1: f"MyTopicName{short_uid()}", + name2: f"MyNewTopicName{short_uid()}", + }, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + name2, + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + + capture_update_process(t1, t2) + + @markers.aws.validated + def test_mappings_with_parameter_lookup( + self, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via looking up a static value in a mapping but the key comes from + a template parameter + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The same conclusions as `test_mappings_with_static_fields` + """ + name1 = "key1" + name2 = "key2" + t1 = { + "Parameters": { + "TopicName": { + "Type": "String", + }, + }, + "Mappings": { + "MyMap": { + "MyKey": { + name1: f"topic-1-{short_uid()}", + name2: f"topic-2-{short_uid()}", + }, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + { + "Ref": "TopicName", + }, + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + + capture_update_process(t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) + + @markers.aws.validated + def test_conditions( + self, + capture_update_process, + ): + """ + Toggle a resource from present to not present via a condition + + Conclusions: + - Adding the second resource creates an `Add` resource change + """ + t1 = { + "Parameters": { + "EnvironmentType": { + "Type": "String", + } + }, + "Conditions": { + "IsProduction": { + "Fn::Equals": [ + {"Ref": "EnvironmentType"}, + "prod", + ], + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "test", + }, + "Condition": "IsProduction", + }, + }, + } + + capture_update_process( + t1, t1, p1={"EnvironmentType": "not-prod"}, p2={"EnvironmentType": "prod"} + ) + + @markers.aws.validated + def test_unrelated_changes_update_propagation( + self, + capture_update_process, + ): + """ + - Resource B depends on resource A which is updated, but the referenced parameter does not + change + + Conclusions: + - No update to resource B + """ + topic_name = f"MyTopic{short_uid()}" + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": topic_name, + "Description": "original", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + + t2 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": topic_name, + "Description": "changed", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + capture_update_process(t1, t2) + + @markers.aws.validated + def test_unrelated_changes_requires_replacement( + self, + capture_update_process, + ): + """ + - Resource B depends on resource A which is updated, but the referenced parameter does not + change, however resource A requires replacement + + Conclusions: + - Resource B is updated + """ + parameter_name_1 = f"MyParameter{short_uid()}" + parameter_name_2 = f"MyParameter{short_uid()}" + + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name_1, + "Type": "String", + "Value": "value", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + t2 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name_2, + "Type": "String", + "Value": "value", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + + capture_update_process(t1, t2) diff --git a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json index b3b80db8dd4fa..ae7ab8f10c674 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json @@ -498,5 +498,4407 @@ } } } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_direct_update": { + "recorded-date": "27-03-2025, 15:27:22", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", + "ChangeSetName": "cs-4eddd7ee", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1-699c8a3c" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", + "ChangeSetName": "cs-4eddd7ee", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", + "ChangeSetName": "cs-4eddd7ee", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2-c8bc3e89" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1-699c8a3c" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2-c8bc3e89", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-699c8a3c", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", + "ChangeSetName": "cs-4eddd7ee", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-a48cca6f-40de-4a25-99f5-f9b9ec67da45", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-74d62981-96eb-4499-97bd-1de15cca0f73", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-c8bc3e89", + "ResourceProperties": { + "TopicName": "topic-2-c8bc3e89" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-c8bc3e89", + "ResourceProperties": { + "TopicName": "topic-2-c8bc3e89" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "ResourceProperties": { + "TopicName": "topic-2-c8bc3e89" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "ResourceProperties": { + "TopicName": "topic-1-699c8a3c" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "ResourceProperties": { + "TopicName": "topic-1-699c8a3c" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1-699c8a3c" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + } + ], + "stack-1ec85b6e": [ + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-1ec85b6e", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "StackName": "stack-1ec85b6e", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { + "recorded-date": "27-03-2025, 15:29:21", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", + "ChangeSetName": "cs-7ca0678c", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1-efcd7dc5" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", + "ChangeSetName": "cs-7ca0678c", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", + "ChangeSetName": "cs-7ca0678c", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2-82db7bdb" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1-efcd7dc5" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2-82db7bdb", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-efcd7dc5", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-1-efcd7dc5", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-efcd7dc5", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-efcd7dc5", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", + "ChangeSetName": "cs-7ca0678c", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-d526ef1f-2313-46f1-822d-917598c7fb75", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-9b9f85bf-2ddb-4098-b926-dc3b12359989", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-82db7bdb", + "ResourceProperties": { + "TopicName": "topic-2-82db7bdb" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-82db7bdb", + "ResourceProperties": { + "TopicName": "topic-2-82db7bdb" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "ResourceProperties": { + "TopicName": "topic-2-82db7bdb" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "ResourceProperties": { + "TopicName": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "ResourceProperties": { + "TopicName": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-82db7bdb" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-82db7bdb" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-efcd7dc5" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + } + ], + "stack-097cd147": [ + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-097cd147", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "StackName": "stack-097cd147", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_parameter_changes": { + "recorded-date": "27-03-2025, 15:31:22", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", + "ChangeSetName": "cs-5be8adbc", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1-fa993773" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1-fa993773" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", + "ChangeSetName": "cs-5be8adbc", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1-fa993773" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1-fa993773" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", + "ChangeSetName": "cs-5be8adbc", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2-c7672df9" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1-fa993773" + } + }, + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2-c7672df9", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-fa993773", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-2-c7672df9", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-fa993773", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-1-fa993773", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-fa993773", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-fa993773", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2-c7672df9" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", + "ChangeSetName": "cs-5be8adbc", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2-c7672df9" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2-c7672df9" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-d28e4326-c3ef-4d26-aab1-592325e91de8", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-e94fb6b6-124f-4998-b93b-67032802c460", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-c7672df9", + "ResourceProperties": { + "TopicName": "topic-2-c7672df9" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-c7672df9", + "ResourceProperties": { + "TopicName": "topic-2-c7672df9" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "ResourceProperties": { + "TopicName": "topic-2-c7672df9" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "ResourceProperties": { + "TopicName": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "ResourceProperties": { + "TopicName": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-c7672df9" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-c7672df9" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-fa993773" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + } + ], + "stack-7e33c12d": [ + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-7e33c12d", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2-c7672df9" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "StackName": "stack-7e33c12d", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { + "recorded-date": "27-03-2025, 15:33:23", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", + "ChangeSetName": "cs-98b12539", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "MyTopicName" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", + "ChangeSetName": "cs-98b12539", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", + "ChangeSetName": "cs-98b12539", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "MyNewTopicName981a23e2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "MyTopicName" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "MyNewTopicName981a23e2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "MyTopicName", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "MyTopicName", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "MyTopicName", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "MyTopicName", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", + "ChangeSetName": "cs-98b12539", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-b04ac465-abfd-462a-99ee-64eee54e03bf", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-641ad9a6-b755-4525-a678-b97a5df4f528", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyNewTopicName981a23e2", + "ResourceProperties": { + "TopicName": "MyNewTopicName981a23e2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyNewTopicName981a23e2", + "ResourceProperties": { + "TopicName": "MyNewTopicName981a23e2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "ResourceProperties": { + "TopicName": "MyNewTopicName981a23e2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "ResourceProperties": { + "TopicName": "MyTopicName" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "ResourceProperties": { + "TopicName": "MyTopicName" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "MyTopicName" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "ResourceProperties": { + "Type": "String", + "Value": "MyNewTopicName981a23e2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "ResourceProperties": { + "Type": "String", + "Value": "MyNewTopicName981a23e2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopicName" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopicName" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopicName" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + } + ], + "stack-32de1443": [ + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-32de1443", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "StackName": "stack-32de1443", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_conditions": { + "recorded-date": "27-03-2025, 15:34:24", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", + "ChangeSetName": "cs-45c519bb", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": {} + }, + "Details": [], + "LogicalResourceId": "Bucket", + "Replacement": "True", + "ResourceType": "AWS::S3::Bucket", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", + "ChangeSetName": "cs-45c519bb", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Bucket", + "ResourceType": "AWS::S3::Bucket", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", + "ChangeSetName": "cs-45c519bb", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "test", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", + "ChangeSetName": "cs-45c519bb", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Bucket": [ + { + "EventId": "Bucket-CREATE_COMPLETE-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "stack-60c7d006-bucket-f9mgwephfnj6", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "Bucket-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "stack-60c7d006-bucket-f9mgwephfnj6", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "Bucket-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-BrUaQwZmp5R5", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-BrUaQwZmp5R5", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + } + ], + "stack-60c7d006": [ + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-60c7d006", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "StackName": "stack-60c7d006", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { + "recorded-date": "27-03-2025, 15:34:51", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", + "ChangeSetName": "cs-67acddb8", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "MyTopiceb1687cb", + "Type": "String", + "Description": "original" + } + }, + "Details": [], + "LogicalResourceId": "Parameter1", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter2", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", + "ChangeSetName": "cs-67acddb8", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", + "ChangeSetName": "cs-67acddb8", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "MyTopiceb1687cb", + "Type": "String", + "Description": "changed" + } + }, + "BeforeContext": { + "Properties": { + "Value": "MyTopiceb1687cb", + "Type": "String", + "Description": "original" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "changed", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "original", + "Name": "Description", + "Path": "/Properties/Description", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", + "ChangeSetName": "cs-67acddb8", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Description", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Parameter1.Value", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter1": [ + { + "EventId": "Parameter1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "ResourceProperties": { + "Type": "String", + "Description": "changed", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "ResourceProperties": { + "Type": "String", + "Description": "changed", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "ResourceProperties": { + "Type": "String", + "Description": "original", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "ResourceProperties": { + "Type": "String", + "Description": "original", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Description": "original", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + } + ], + "Parameter2": [ + { + "EventId": "Parameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "MyTopiceb1687cb" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + } + ], + "stack-bdbd0fde": [ + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-bdbd0fde", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "StackName": "stack-bdbd0fde", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_requires_replacement": { + "recorded-date": "27-03-2025, 15:35:20", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", + "ChangeSetName": "cs-3cecf361", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value", + "Type": "String", + "Name": "MyParameter9469f4fe" + } + }, + "Details": [], + "LogicalResourceId": "Parameter1", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter2", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", + "ChangeSetName": "cs-3cecf361", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", + "ChangeSetName": "cs-3cecf361", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value", + "Type": "String", + "Name": "MyParameter989f40e8" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value", + "Type": "String", + "Name": "MyParameter9469f4fe" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "MyParameter989f40e8", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "MyParameter9469f4fe", + "Name": "Name", + "Path": "/Properties/Name", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value", + "Type": "String" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "CausingEntity": "Parameter1.Value", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", + "ChangeSetName": "cs-3cecf361", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Name", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Parameter1.Value", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter1": [ + { + "EventId": "Parameter1-b6fe9c4b-ee5a-4ff1-bd74-a97f2ac48892", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-bfc08cb6-1a40-471f-8598-88f0bd62289a", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter989f40e8", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter989f40e8" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter989f40e8", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter989f40e8" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter989f40e8" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter9469f4fe" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "MyParameter9469f4fe", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter9469f4fe" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value", + "Name": "MyParameter9469f4fe" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + } + ], + "Parameter2": [ + { + "EventId": "Parameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "ResourceProperties": { + "Type": "String", + "Value": "value" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "ResourceProperties": { + "Type": "String", + "Value": "value" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + } + ], + "stack-859f670a": [ + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-859f670a", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "StackName": "stack-859f670a", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { + "recorded-date": "27-03-2025, 15:43:03", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", + "ChangeSetName": "cs-67879e0f", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1-867f6b77" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", + "ChangeSetName": "cs-67879e0f", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", + "ChangeSetName": "cs-67879e0f", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2-8e2a426b" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1-867f6b77" + } + }, + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2-8e2a426b", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-867f6b77", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-2-8e2a426b", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-867f6b77", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-1-867f6b77", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-867f6b77", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1-867f6b77", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", + "ChangeSetName": "cs-67879e0f", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-329b4b0b-ea37-4fc1-9c0a-6fd7405bdaa0", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-eaeea20b-89f6-4f3b-a36d-412ef7b36c71", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-8e2a426b", + "ResourceProperties": { + "TopicName": "topic-2-8e2a426b" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2-8e2a426b", + "ResourceProperties": { + "TopicName": "topic-2-8e2a426b" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "ResourceProperties": { + "TopicName": "topic-2-8e2a426b" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "ResourceProperties": { + "TopicName": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "ResourceProperties": { + "TopicName": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-8e2a426b" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2-8e2a426b" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1-867f6b77" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + } + ], + "stack-ac935456": [ + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "stack-ac935456", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "StackName": "stack-ac935456", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } } } diff --git a/tests/aws/services/cloudformation/api/test_changesets.validation.json b/tests/aws/services/cloudformation/api/test_changesets.validation.json index 0e46e94326bff..949d5bbad61a3 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.validation.json +++ b/tests/aws/services/cloudformation/api/test_changesets.validation.json @@ -1,4 +1,28 @@ { + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_conditions": { + "last_validated_date": "2025-03-27T15:34:24+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_direct_update": { + "last_validated_date": "2025-03-27T15:27:22+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { + "last_validated_date": "2025-03-27T15:29:21+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { + "last_validated_date": "2025-03-27T15:43:02+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { + "last_validated_date": "2025-03-27T15:33:23+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_parameter_changes": { + "last_validated_date": "2025-03-27T15:31:22+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_requires_replacement": { + "last_validated_date": "2025-03-27T15:35:20+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { + "last_validated_date": "2025-03-27T15:34:51+00:00" + }, "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_update_without_parameters": { "last_validated_date": "2022-05-31T07:32:02+00:00" }, From 04af66967cf8083f69eeb79d934e624652e43d0b Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:54:21 +0200 Subject: [PATCH 006/108] CloudFormation: POC Support Update Graph Modeling of Mappings and FindInMap (#12432) --- .../engine/v2/change_set_model.py | 118 ++++++++- .../engine/v2/change_set_model_describer.py | 45 +++- .../engine/v2/change_set_model_visitor.py | 13 + .../test_change_set_describe_details.py | 232 ++++++++++++++++++ 4 files changed, 406 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 7601cd0566773..426b44410f7c7 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -113,6 +113,7 @@ class ChangeSetTerminal(ChangeSetEntity, abc.ABC): ... class NodeTemplate(ChangeSetNode): + mappings: Final[NodeMappings] parameters: Final[NodeParameters] conditions: Final[NodeConditions] resources: Final[NodeResources] @@ -121,11 +122,13 @@ def __init__( self, scope: Scope, change_type: ChangeType, + mappings: NodeMappings, parameters: NodeParameters, conditions: NodeConditions, resources: NodeResources, ): super().__init__(scope=scope, change_type=change_type) + self.mappings = mappings self.parameters = parameters self.conditions = conditions self.resources = resources @@ -168,6 +171,24 @@ def __init__(self, scope: Scope, change_type: ChangeType, parameters: list[NodeP self.parameters = parameters +class NodeMapping(ChangeSetNode): + name: Final[str] + bindings: Final[NodeObject] + + def __init__(self, scope: Scope, change_type: ChangeType, name: str, bindings: NodeObject): + super().__init__(scope=scope, change_type=change_type) + self.name = name + self.bindings = bindings + + +class NodeMappings(ChangeSetNode): + mappings: Final[list[NodeMapping]] + + def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMapping]): + super().__init__(scope=scope, change_type=change_type) + self.mappings = mappings + + class NodeCondition(ChangeSetNode): name: Final[str] body: Final[ChangeSetEntity] @@ -300,6 +321,7 @@ def __init__(self, scope: Scope, value: Any): TypeKey: Final[str] = "Type" ConditionKey: Final[str] = "Condition" ConditionsKey: Final[str] = "Conditions" +MappingsKey: Final[str] = "Mappings" ResourcesKey: Final[str] = "Resources" PropertiesKey: Final[str] = "Properties" ParametersKey: Final[str] = "Parameters" @@ -309,7 +331,15 @@ def __init__(self, scope: Scope, value: Any): FnNot: Final[str] = "Fn::Not" FnGetAttKey: Final[str] = "Fn::GetAtt" FnEqualsKey: Final[str] = "Fn::Equals" -INTRINSIC_FUNCTIONS: Final[set[str]] = {RefKey, FnIf, FnNot, FnEqualsKey, FnGetAttKey} +FnFindInMapKey: Final[str] = "Fn::FindInMap" +INTRINSIC_FUNCTIONS: Final[set[str]] = { + RefKey, + FnIf, + FnNot, + FnEqualsKey, + FnGetAttKey, + FnFindInMapKey, +} class ChangeSetModel: @@ -455,6 +485,36 @@ def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeT node_resource = self._retrieve_or_visit_resource(resource_name=logical_id) return node_resource.change_type + def _resolve_intrinsic_function_fn_find_in_map(self, arguments: ChangeSetEntity) -> ChangeType: + if arguments.change_type != ChangeType.UNCHANGED: + return arguments.change_type + # TODO: validate arguments structure and type. + # TODO: add support for nested functions, here we assume the arguments are string literals. + + if not isinstance(arguments, NodeArray) or not arguments.array: + raise RuntimeError() + argument_mapping_name = arguments.array[0] + if not isinstance(argument_mapping_name, TerminalValue): + raise NotImplementedError() + argument_top_level_key = arguments.array[1] + if not isinstance(argument_top_level_key, TerminalValue): + raise NotImplementedError() + argument_second_level_key = arguments.array[2] + if not isinstance(argument_second_level_key, TerminalValue): + raise NotImplementedError() + mapping_name = argument_mapping_name.value + top_level_key = argument_top_level_key.value + second_level_key = argument_second_level_key.value + + node_mapping = self._retrieve_mapping(mapping_name=mapping_name) + # TODO: a lookup would be beneficial in this scenario too; + # consider implications downstream and for replication. + top_level_object = node_mapping.bindings.bindings.get(top_level_key) + if not isinstance(top_level_object, NodeObject): + raise RuntimeError() + target_map_value = top_level_object.bindings.get(second_level_key) + return target_map_value.change_type + def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> ChangeType: # TODO: validate arguments structure and type. if not isinstance(arguments, NodeArray) or not arguments.array: @@ -705,6 +765,36 @@ def _visit_resources( change_type = change_type.for_child(resource.change_type) return NodeResources(scope=scope, change_type=change_type, resources=resources) + def _visit_mapping( + self, scope: Scope, name: str, before_mapping: Maybe[dict], after_mapping: Maybe[dict] + ) -> NodeMapping: + bindings = self._visit_object( + scope=scope, before_object=before_mapping, after_object=after_mapping + ) + return NodeMapping( + scope=scope, change_type=bindings.change_type, name=name, bindings=bindings + ) + + def _visit_mappings( + self, scope: Scope, before_mappings: Maybe[dict], after_mappings: Maybe[dict] + ) -> NodeMappings: + change_type = ChangeType.UNCHANGED + mappings: list[NodeMapping] = list() + mapping_names = self._safe_keys_of(before_mappings, after_mappings) + for mapping_name in mapping_names: + scope_mapping, (before_mapping, after_mapping) = self._safe_access_in( + scope, mapping_name, before_mappings, after_mappings + ) + mapping = self._visit_mapping( + scope=scope, + name=mapping_name, + before_mapping=before_mapping, + after_mapping=after_mapping, + ) + mappings.append(mapping) + change_type = change_type.for_child(mapping.change_type) + return NodeMappings(scope=scope, change_type=change_type, mappings=mappings) + def _visit_dynamic_parameter(self, parameter_name: str) -> ChangeSetEntity: scope = Scope("Dynamic").open_scope("Parameters") scope_parameter, (before_parameter, after_parameter) = self._safe_access_in( @@ -845,6 +935,14 @@ def _visit_conditions( def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate: root_scope = Scope() # TODO: visit other child types + + mappings_scope, (before_mappings, after_mappings) = self._safe_access_in( + root_scope, MappingsKey, before_template, after_template + ) + mappings = self._visit_mappings( + scope=mappings_scope, before_mappings=before_mappings, after_mappings=after_mappings + ) + parameters_scope, (before_parameters, after_parameters) = self._safe_access_in( root_scope, ParametersKey, before_template, after_template ) @@ -876,6 +974,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N return NodeTemplate( scope=root_scope, change_type=resources.change_type, + mappings=mappings, parameters=parameters, conditions=conditions, resources=resources, @@ -919,6 +1018,23 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar return node_parameter return None + def _retrieve_mapping(self, mapping_name) -> NodeMapping: + # TODO: add caching mechanism, and raise appropriate error if missing. + scope_mappings, (before_mappings, after_mappings) = self._safe_access_in( + Scope(), MappingsKey, self._before_template, self._after_template + ) + before_mappings = before_mappings or dict() + after_mappings = after_mappings or dict() + if mapping_name in before_mappings or mapping_name in after_mappings: + scope_mapping, (before_mapping, after_mapping) = self._safe_access_in( + scope_mappings, mapping_name, before_mappings, after_mappings + ) + node_mapping = self._visit_mapping( + scope_mapping, mapping_name, before_mapping, after_mapping + ) + return node_mapping + raise RuntimeError() + def _retrieve_or_visit_resource(self, resource_name: str) -> NodeResource: resources_scope, (before_resources, after_resources) = self._safe_access_in( Scope(), diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index 9301af7729899..fbda4d6c3fa5f 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -11,6 +11,7 @@ NodeCondition, NodeDivergence, NodeIntrinsicFunction, + NodeMapping, NodeObject, NodeParameter, NodeProperties, @@ -74,6 +75,15 @@ def _get_node_property_for(property_name: str, node_resource: NodeResource) -> N # TODO raise RuntimeError() + def _get_node_mapping(self, map_name: str) -> NodeMapping: + mappings: list[NodeMapping] = self._node_template.mappings.mappings + # TODO: another scenarios suggesting property lookups might be preferable. + for mapping in mappings: + if mapping.name == map_name: + return mapping + # TODO + raise RuntimeError() + def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]: parameters: list[NodeParameter] = self._node_template.parameters.parameters # TODO: another scenarios suggesting property lookups might be preferable. @@ -109,6 +119,16 @@ def _resolve_reference(self, logica_id: str) -> DescribeUnit: resource_unit = DescribeUnit(before_context=limitation_str, after_context=limitation_str) return resource_unit + def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit: + # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids. + node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name) + top_level_value = node_mapping.bindings.bindings.get(top_level_key) + if not isinstance(top_level_value, NodeObject): + raise RuntimeError() + second_level_value = top_level_value.bindings.get(second_level_key) + mapping_value_unit = self.visit(second_level_value) + return mapping_value_unit + def _resolve_reference_binding( self, before_logical_id: str, after_logical_id: str ) -> DescribeUnit: @@ -281,8 +301,31 @@ def visit_node_intrinsic_function_fn_not( # Implicit change type computation. return DescribeUnit(before_context=before_context, after_context=after_context) + def visit_node_intrinsic_function_fn_find_in_map( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> DescribeUnit: + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + # TODO: add type checking/validation for result unit? + arguments_unit = self.visit(node_intrinsic_function.arguments) + before_arguments = arguments_unit.before_context + after_arguments = arguments_unit.after_context + if before_arguments: + before_value_unit = self._resolve_mapping(*before_arguments) + before_context = before_value_unit.before_context + else: + before_context = None + if after_arguments: + after_value_unit = self._resolve_mapping(*after_arguments) + after_context = after_value_unit.after_context + else: + after_context = None + return DescribeUnit(before_context=before_context, after_context=after_context) + + def visit_node_mapping(self, node_mapping: NodeMapping) -> DescribeUnit: + bindings_unit = self.visit(node_mapping.bindings) + return bindings_unit + def visit_node_parameter(self, node_parameter: NodeParameter) -> DescribeUnit: - # TODO: add caching for these operation, parameters may be referenced more than once. # TODO: add support for default value sampling dynamic_value = node_parameter.dynamic_value describe_unit = self.visit(dynamic_value) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py index 39ef67e912313..8a167979fb177 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py @@ -7,6 +7,8 @@ NodeConditions, NodeDivergence, NodeIntrinsicFunction, + NodeMapping, + NodeMappings, NodeObject, NodeParameter, NodeParameters, @@ -45,6 +47,12 @@ def visit_children(self, change_set_entity: ChangeSetEntity): def visit_node_template(self, node_template: NodeTemplate): self.visit_children(node_template) + def visit_node_mapping(self, node_mapping: NodeMapping): + self.visit_children(node_mapping) + + def visit_node_mappings(self, node_mappings: NodeMappings): + self.visit_children(node_mappings) + def visit_node_parameters(self, node_parameters: NodeParameters): self.visit_children(node_parameters) @@ -94,6 +102,11 @@ def visit_node_intrinsic_function_fn_if(self, node_intrinsic_function: NodeIntri def visit_node_intrinsic_function_fn_not(self, node_intrinsic_function: NodeIntrinsicFunction): self.visit_children(node_intrinsic_function) + def visit_node_intrinsic_function_fn_find_in_map( + self, node_intrinsic_function: NodeIntrinsicFunction + ): + self.visit_children(node_intrinsic_function) + def visit_node_intrinsic_function_ref(self, node_intrinsic_function: NodeIntrinsicFunction): self.visit_children(node_intrinsic_function) diff --git a/tests/unit/services/cloudformation/test_change_set_describe_details.py b/tests/unit/services/cloudformation/test_change_set_describe_details.py index f5d3c8143bba8..d8cf98ff5b757 100644 --- a/tests/unit/services/cloudformation/test_change_set_describe_details.py +++ b/tests/unit/services/cloudformation/test_change_set_describe_details.py @@ -1222,3 +1222,235 @@ def test_condition_update_production_remove_resource(self): } ] self.compare_changes(changes, target) + + def test_mappings_update_string_referencing_resource(self): + t1 = { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] + }, + }, + } + }, + } + t2 = { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-2"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] + }, + }, + } + }, + } + changes = self.eval_change_set(t1, t2) + target = [ + { + "Type": "Resource", + "ResourceChange": { + "Action": "Modify", + "LogicalResourceId": "MySSMParameter", + # "PhysicalResourceId": "", + "ResourceType": "AWS::SSM::Parameter", + # "Replacement": "False", + # "Scope": [ + # "Properties" + # ], + # "Details": [ + # { + # "Target": { + # "Attribute": "Properties", + # "Name": "Value", + # "RequiresRecreation": "Never", + # "Path": "/Properties/Value", + # "BeforeValue": "value-1", + # "AfterValue": "value-2", + # "AttributeChangeType": "Modify" + # }, + # "Evaluation": "Static", + # "ChangeSource": "DirectModification" + # } + # ], + "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, + "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, + }, + } + ] + self.compare_changes(changes, target) + + def test_mappings_update_type_referencing_resource(self): + t1 = { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] + }, + }, + } + }, + } + t2 = { + "Mappings": { + "GenericMapping": {"EnvironmentA": {"ParameterValue": ["value-1", "value-2"]}} + }, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] + }, + }, + } + }, + } + changes = self.eval_change_set(t1, t2) + target = [ + { + "Type": "Resource", + "ResourceChange": { + "Action": "Modify", + "LogicalResourceId": "MySSMParameter", + # "PhysicalResourceId": "", + "ResourceType": "AWS::SSM::Parameter", + # "Replacement": "False", + # "Scope": [ + # "Properties" + # ], + # "Details": [ + # { + # "Target": { + # "Attribute": "Properties", + # "Name": "Value", + # "RequiresRecreation": "Never", + # "Path": "/Properties/Value", + # "BeforeValue": "value-1", + # "AfterValue": "[value-1, value-2]", + # "AttributeChangeType": "Modify" + # }, + # "Evaluation": "Static", + # "ChangeSource": "DirectModification" + # } + # ], + "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, + "AfterContext": { + "Properties": {"Value": ["value-1", "value-2"], "Type": "String"} + }, + }, + } + ] + self.compare_changes(changes, target) + + @pytest.mark.skip(reason="Add support for nested intrinsic functions") + def test_mappings_update_referencing_resource_through_parameter(self): + t1 = { + "Parameters": { + "Environment": { + "Type": "String", + "AllowedValues": [ + "EnvironmentA", + ], + } + }, + "Mappings": { + "GenericMapping": { + "EnvironmentA": {"ParameterValue": "value-1"}, + "EnvironmentB": {"ParameterValue": "value-2"}, + } + }, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + {"Ref": "Environment"}, + "ParameterValue", + ] + }, + }, + } + }, + } + t2 = { + "Parameters": { + "Environment": { + "Type": "String", + "AllowedValues": ["EnvironmentA", "EnvironmentB"], + "Default": "EnvironmentA", + } + }, + "Mappings": { + "GenericMapping": { + "EnvironmentA": {"ParameterValue": "value-1-2"}, + "EnvironmentB": {"ParameterValue": "value-2"}, + } + }, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + {"Ref": "Environment"}, + "ParameterValue", + ] + }, + }, + } + }, + } + changes = self.eval_change_set( + t1, t2, {"Environment": "EnvironmentA"}, {"Environment": "EnvironmentA"} + ) + target = [ + { + "Type": "Resource", + "ResourceChange": { + "Action": "Modify", + "LogicalResourceId": "MySSMParameter", + # "PhysicalResourceId": "", + "ResourceType": "AWS::SSM::Parameter", + # "Replacement": "False", + # "Scope": [ + # "Properties" + # ], + # "Details": [ + # { + # "Target": { + # "Attribute": "Properties", + # "Name": "Value", + # "RequiresRecreation": "Never", + # "Path": "/Properties/Value", + # "BeforeValue": "value-1", + # "AfterValue": "value-1-2", + # "AttributeChangeType": "Modify" + # }, + # "Evaluation": "Static", + # "ChangeSource": "DirectModification" + # } + # ], + "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, + "AfterContext": {"Properties": {"Value": "value-1-2", "Type": "String"}}, + }, + } + ] + self.compare_changes(changes, target) From 9d62e252eb4e0cebca9ee2a0517f1c9477531c13 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Mon, 31 Mar 2025 13:22:58 +0000 Subject: [PATCH 007/108] Admin: Improve license metadata (#12455) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c57a71eda550a..af06383e3b382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dynamic = ["version"] classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", - "License :: OSI Approved :: Apache Software License", + "License-Expression :: OSI Approved :: Apache Software License", "Topic :: Internet", "Topic :: Software Development :: Testing", "Topic :: System :: Emulators", From c6340e2b9e4526a961af3be22c4090a999b9a0ec Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:24:18 +0200 Subject: [PATCH 008/108] Update CODEOWNERS (#12454) Co-authored-by: LocalStack Bot --- CODEOWNERS | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1137baefe445c..9f806378e7f42 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -185,7 +185,7 @@ /tests/unit/services/opensearch/ @alexrashed @silv-io # pipes -/localstack-core/localstack/aws/api/pipes/ @joe4dev @gregfurman +/localstack-core/localstack/aws/api/pipes/ @tiurin @gregfurman @joe4dev # route53 /localstack-core/localstack/aws/api/route53/ @giograno @@ -208,11 +208,6 @@ /localstack-core/localstack/services/s3control/ @bentsku /tests/aws/services/s3control/ @bentsku -# scheduler -/localstack-core/localstack/aws/api/scheduler/ @zaingz @joe4dev -/localstack-core/localstack/services/scheduler/ @zaingz @joe4dev -/tests/aws/services/scheduler/ @zaingz @joe4dev - # secretsmanager /localstack-core/localstack/aws/api/secretsmanager/ @dominikschubert @macnev2013 @MEPalma /localstack-core/localstack/services/secretsmanager/ @dominikschubert @macnev2013 @MEPalma From 460091049e704a69dec4e17c414f7395cd366108 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:19:49 +0200 Subject: [PATCH 009/108] CloudFormation: POC Support for Modeling of Outputs Blocks in the Update Graph, Improved Handling of Intrinsic Function Types (#12443) --- .../engine/v2/change_set_model.py | 177 +++++++++++--- .../engine/v2/change_set_model_describer.py | 202 +++++++++++----- .../engine/v2/change_set_model_visitor.py | 8 + .../test_change_set_describe_details.py | 228 +++++++++++++++++- 4 files changed, 517 insertions(+), 98 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 426b44410f7c7..8995624c50d7c 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -117,6 +117,7 @@ class NodeTemplate(ChangeSetNode): parameters: Final[NodeParameters] conditions: Final[NodeConditions] resources: Final[NodeResources] + outputs: Final[NodeOutputs] def __init__( self, @@ -126,12 +127,14 @@ def __init__( parameters: NodeParameters, conditions: NodeConditions, resources: NodeResources, + outputs: NodeOutputs, ): super().__init__(scope=scope, change_type=change_type) self.mappings = mappings self.parameters = parameters self.conditions = conditions self.resources = resources + self.outputs = outputs class NodeDivergence(ChangeSetNode): @@ -189,6 +192,36 @@ def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMap self.mappings = mappings +class NodeOutput(ChangeSetNode): + name: Final[str] + value: Final[ChangeSetEntity] + export: Final[Optional[ChangeSetEntity]] + condition_reference: Final[Optional[TerminalValue]] + + def __init__( + self, + scope: Scope, + change_type: ChangeType, + name: str, + value: ChangeSetEntity, + export: Optional[ChangeSetEntity], + conditional_reference: Optional[TerminalValue], + ): + super().__init__(scope=scope, change_type=change_type) + self.name = name + self.value = value + self.export = export + self.condition_reference = conditional_reference + + +class NodeOutputs(ChangeSetNode): + outputs: Final[list[NodeOutput]] + + def __init__(self, scope: Scope, change_type: ChangeType, outputs: list[NodeOutput]): + super().__init__(scope=scope, change_type=change_type) + self.outputs = outputs + + class NodeCondition(ChangeSetNode): name: Final[str] body: Final[ChangeSetEntity] @@ -218,7 +251,7 @@ def __init__(self, scope: Scope, change_type: ChangeType, resources: list[NodeRe class NodeResource(ChangeSetNode): name: Final[str] type_: Final[ChangeSetTerminal] - condition_reference: Final[TerminalValue] + condition_reference: Final[Optional[TerminalValue]] properties: Final[NodeProperties] def __init__( @@ -325,6 +358,9 @@ def __init__(self, scope: Scope, value: Any): ResourcesKey: Final[str] = "Resources" PropertiesKey: Final[str] = "Properties" ParametersKey: Final[str] = "Parameters" +ValueKey: Final[str] = "Value" +ExportKey: Final[str] = "Export" +OutputsKey: Final[str] = "Outputs" # TODO: expand intrinsic functions set. RefKey: Final[str] = "Ref" FnIf: Final[str] = "Fn::If" @@ -567,17 +603,9 @@ def _visit_object( binding_scope, (before_value, after_value) = self._safe_access_in( scope, binding_name, before_object, after_object ) - if self._is_intrinsic_function_name(function_name=binding_name): - value = self._visit_intrinsic_function( - scope=binding_scope, - intrinsic_function=binding_name, - before_arguments=before_value, - after_arguments=after_value, - ) - else: - value = self._visit_value( - scope=binding_scope, before_value=before_value, after_value=after_value - ) + value = self._visit_value( + scope=binding_scope, before_value=before_value, after_value=after_value + ) bindings[binding_name] = value change_type = change_type.for_child(value.change_type) node_object = NodeObject(scope=scope, change_type=change_type, bindings=bindings) @@ -601,8 +629,11 @@ def _visit_value( value = self._visited_scopes.get(scope) if isinstance(value, ChangeSetEntity): return value + + before_type_name = self._type_name_of(before_value) + after_type_name = self._type_name_of(after_value) unset = object() - if type(before_value) is type(after_value): + if before_type_name == after_type_name: dominant_value = before_value elif self._is_created(before=before_value, after=after_value): dominant_value = after_value @@ -611,6 +642,7 @@ def _visit_value( else: dominant_value = unset if dominant_value is not unset: + dominant_type_name = self._type_name_of(dominant_value) if self._is_terminal(value=dominant_value): value = self._visit_terminal_value( scope=scope, before_value=before_value, after_value=after_value @@ -623,6 +655,16 @@ def _visit_value( value = self._visit_array( scope=scope, before_array=before_value, after_array=after_value ) + elif self._is_intrinsic_function_name(dominant_type_name): + intrinsic_function_scope, (before_arguments, after_arguments) = ( + self._safe_access_in(scope, dominant_type_name, before_value, after_value) + ) + value = self._visit_intrinsic_function( + scope=scope, + intrinsic_function=dominant_type_name, + before_arguments=before_arguments, + after_arguments=after_arguments, + ) else: raise RuntimeError(f"Unsupported type {type(dominant_value)}") # Case: type divergence. @@ -717,12 +759,15 @@ def _visit_resource( # TODO: investigate behaviour with type changes, for now this is filler code. _, type_str = self._safe_access_in(scope, TypeKey, before_resource) + condition_reference = None scope_condition, (before_condition, after_condition) = self._safe_access_in( scope, ConditionKey, before_resource, after_resource ) - condition_reference = self._visit_terminal_value( - scope_condition, before_condition, after_condition - ) + # TODO: condition references should be resolved for the condition's change_type? + if before_condition or after_condition: + condition_reference = self._visit_terminal_value( + scope_condition, before_condition, after_condition + ) scope_properties, (before_properties, after_properties) = self._safe_access_in( scope, PropertiesKey, before_resource, after_resource @@ -887,18 +932,9 @@ def _visit_condition( node_condition = self._visited_scopes.get(scope) if isinstance(node_condition, NodeCondition): return node_condition - - # TODO: is schema validation/check necessary or can we trust the input at this point? - function_names: list[str] = self._safe_keys_of(before_condition, after_condition) - if len(function_names) == 1: - body = self._visit_object( - scope=scope, before_object=before_condition, after_object=after_condition - ) - else: - body = self._visit_divergence( - scope=scope, before_value=before_condition, after_value=after_condition - ) - + body = self._visit_value( + scope=scope, before_value=before_condition, after_value=after_condition + ) node_condition = NodeCondition( scope=scope, change_type=body.change_type, name=condition_name, body=body ) @@ -932,6 +968,64 @@ def _visit_conditions( self._visited_scopes[scope] = node_conditions return node_conditions + def _visit_output( + self, scope: Scope, name: str, before_output: Maybe[dict], after_output: Maybe[dict] + ) -> NodeOutput: + change_type = ChangeType.UNCHANGED + scope_value, (before_value, after_value) = self._safe_access_in( + scope, ValueKey, before_output, after_output + ) + value = self._visit_value(scope_value, before_value, after_value) + change_type = change_type.for_child(value.change_type) + + export: Optional[ChangeSetEntity] = None + scope_export, (before_export, after_export) = self._safe_access_in( + scope, ExportKey, before_output, after_output + ) + if before_export or after_export: + export = self._visit_value(scope_export, before_export, after_export) + change_type = change_type.for_child(export.change_type) + + # TODO: condition references should be resolved for the condition's change_type? + condition_reference: Optional[TerminalValue] = None + scope_condition, (before_condition, after_condition) = self._safe_access_in( + scope, ConditionKey, before_output, after_output + ) + if before_condition or after_condition: + condition_reference = self._visit_terminal_value( + scope_condition, before_condition, after_condition + ) + change_type = change_type.for_child(condition_reference.change_type) + + return NodeOutput( + scope=scope, + change_type=change_type, + name=name, + value=value, + export=export, + conditional_reference=condition_reference, + ) + + def _visit_outputs( + self, scope: Scope, before_outputs: Maybe[dict], after_outputs: Maybe[dict] + ) -> NodeOutputs: + change_type = ChangeType.UNCHANGED + outputs: list[NodeOutput] = list() + output_names: list[str] = self._safe_keys_of(before_outputs, after_outputs) + for output_name in output_names: + scope_output, (before_output, after_output) = self._safe_access_in( + scope, output_name, before_outputs, after_outputs + ) + output = self._visit_output( + scope=scope_output, + name=output_name, + before_output=before_output, + after_output=after_output, + ) + outputs.append(output) + change_type = change_type.for_child(output.change_type) + return NodeOutputs(scope=scope, change_type=change_type, outputs=outputs) + def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate: root_scope = Scope() # TODO: visit other child types @@ -970,6 +1064,13 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N after_resources=after_resources, ) + outputs_scope, (before_outputs, after_outputs) = self._safe_access_in( + root_scope, OutputsKey, before_template, after_template + ) + outputs = self._visit_outputs( + scope=outputs_scope, before_outputs=before_outputs, after_outputs=after_outputs + ) + # TODO: compute the change_type of the template properly. return NodeTemplate( scope=root_scope, @@ -978,6 +1079,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N parameters=parameters, conditions=conditions, resources=resources, + outputs=outputs, ) def _retrieve_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]: @@ -1090,13 +1192,30 @@ def _change_type_for_parent_of(change_types: list[ChangeType]) -> ChangeType: break return parent_change_type + @staticmethod + def _name_if_intrinsic_function(value: Maybe[Any]) -> Optional[str]: + if isinstance(value, dict): + keys = ChangeSetModel._safe_keys_of(value) + if len(keys) == 1: + key_name = keys[0] + if ChangeSetModel._is_intrinsic_function_name(key_name): + return key_name + return None + + @staticmethod + def _type_name_of(value: Maybe[Any]) -> str: + maybe_intrinsic_function_name = ChangeSetModel._name_if_intrinsic_function(value) + if maybe_intrinsic_function_name is not None: + return maybe_intrinsic_function_name + return type(value).__name__ + @staticmethod def _is_terminal(value: Any) -> bool: return type(value) in {int, float, bool, str, None, NothingType} @staticmethod def _is_object(value: Any) -> bool: - return isinstance(value, dict) + return isinstance(value, dict) and ChangeSetModel._name_if_intrinsic_function(value) is None @staticmethod def _is_array(value: Any) -> bool: diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index fbda4d6c3fa5f..4e9dcdd6369e2 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -7,12 +7,16 @@ from localstack.services.cloudformation.engine.v2.change_set_model import ( ChangeSetEntity, ChangeType, + ConditionKey, + ExportKey, NodeArray, NodeCondition, NodeDivergence, NodeIntrinsicFunction, NodeMapping, NodeObject, + NodeOutput, + NodeOutputs, NodeParameter, NodeProperties, NodeProperty, @@ -26,6 +30,7 @@ TerminalValueModified, TerminalValueRemoved, TerminalValueUnchanged, + ValueKey, ) from localstack.services.cloudformation.engine.v2.change_set_model_visitor import ( ChangeSetModelVisitor, @@ -112,12 +117,13 @@ def _resolve_reference(self, logica_id: str) -> DescribeUnit: return parameter_unit # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - # node_resource = self._get_node_resource_for( - # resource_name=logica_id, node_template=self._node_template - # ) - limitation_str = "Cannot yet compute Ref values for Resources" - resource_unit = DescribeUnit(before_context=limitation_str, after_context=limitation_str) - return resource_unit + node_resource = self._get_node_resource_for( + resource_name=logica_id, node_template=self._node_template + ) + resource_unit = self.visit(node_resource) + before_context = resource_unit.before_context + after_context = resource_unit.after_context + return DescribeUnit(before_context=before_context, after_context=after_context) def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit: # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids. @@ -210,29 +216,37 @@ def visit_node_intrinsic_function_fn_get_att( arguments_unit = self.visit(node_intrinsic_function.arguments) # TODO: validate the return value according to the spec. before_argument_list = arguments_unit.before_context - before_logical_name_of_resource = before_argument_list[0] - before_attribute_name = before_argument_list[1] - before_node_resource = self._get_node_resource_for( - resource_name=before_logical_name_of_resource, node_template=self._node_template - ) - node_property: TerminalValue = self._get_node_property_for( - property_name=before_attribute_name, node_resource=before_node_resource - ) + after_argument_list = arguments_unit.after_context - before_context = node_property.value.value - if node_property.change_type != ChangeType.UNCHANGED: + before_context = None + if before_argument_list: + before_logical_name_of_resource = before_argument_list[0] + before_attribute_name = before_argument_list[1] + before_node_resource = self._get_node_resource_for( + resource_name=before_logical_name_of_resource, node_template=self._node_template + ) + before_node_property = self._get_node_property_for( + property_name=before_attribute_name, node_resource=before_node_resource + ) + before_property_unit = self.visit(before_node_property) + before_context = before_property_unit.before_context + + after_context = None + if after_argument_list: after_context = CHANGESET_KNOWN_AFTER_APPLY - else: - after_context = node_property.value.value + # TODO: the following is the logic to resolve the attribute in the `after` template + # this should be moved to the new base class and then be masked in this describer. + # after_logical_name_of_resource = after_argument_list[0] + # after_attribute_name = after_argument_list[1] + # after_node_resource = self._get_node_resource_for( + # resource_name=after_logical_name_of_resource, node_template=self._node_template + # ) + # after_node_property = self._get_node_property_for( + # property_name=after_attribute_name, node_resource=after_node_resource + # ) + # after_property_unit = self.visit(after_node_property) + # after_context = after_property_unit.after_context - match node_intrinsic_function.change_type: - case ChangeType.MODIFIED: - return DescribeUnit(before_context=before_context, after_context=after_context) - case ChangeType.CREATED: - return DescribeUnit(after_context=after_context) - case ChangeType.REMOVED: - return DescribeUnit(before_context=before_context) - # Unchanged return DescribeUnit(before_context=before_context, after_context=after_context) def visit_node_intrinsic_function_fn_equals( @@ -342,12 +356,16 @@ def visit_node_intrinsic_function_ref( # TODO: add tests with created and deleted parameters and verify this logic holds. before_logical_id = arguments_unit.before_context - before_unit = self._resolve_reference(logica_id=before_logical_id) - before_context = before_unit.before_context + before_context = None + if before_logical_id is not None: + before_unit = self._resolve_reference(logica_id=before_logical_id) + before_context = before_unit.before_context after_logical_id = arguments_unit.after_context - after_unit = self._resolve_reference(logica_id=after_logical_id) - after_context = after_unit.after_context + after_context = None + if after_logical_id is not None: + after_unit = self._resolve_reference(logica_id=after_logical_id) + after_context = after_unit.after_context return DescribeUnit(before_context=before_context, after_context=after_context) @@ -406,21 +424,71 @@ def _resolve_resource_condition_reference(self, reference: TerminalValue) -> Des ) return DescribeUnit(before_context=before_context, after_context=after_context) + def visit_node_output(self, node_output: NodeOutput) -> DescribeUnit: + # This logic is not required for Describe operations, + # and should be ported a new base for this class type. + change_type = node_output.change_type + value_unit = self.visit(node_output.value) + + condition_unit = None + if node_output.condition_reference is not None: + condition_unit = self._resolve_resource_condition_reference( + node_output.condition_reference + ) + condition_before = condition_unit.before_context + condition_after = condition_unit.after_context + if not condition_before and condition_after: + change_type = ChangeType.CREATED + elif condition_before and not condition_after: + change_type = ChangeType.REMOVED + + export_unit = None + if node_output.export is not None: + export_unit = self.visit(node_output.export) + + before_context = None + after_context = None + if change_type != ChangeType.REMOVED: + after_context = {"Name": node_output.name, ValueKey: value_unit.after_context} + if export_unit: + after_context[ExportKey] = export_unit.after_context + if condition_unit: + after_context[ConditionKey] = condition_unit.after_context + if change_type != ChangeType.CREATED: + before_context = {"Name": node_output.name, ValueKey: value_unit.before_context} + if export_unit: + before_context[ExportKey] = export_unit.before_context + if condition_unit: + before_context[ConditionKey] = condition_unit.before_context + return DescribeUnit(before_context=before_context, after_context=after_context) + + def visit_node_outputs(self, node_outputs: NodeOutputs) -> DescribeUnit: + # This logic is not required for Describe operations, + # and should be ported a new base for this class type. + before_context = list() + after_context = list() + for node_output in node_outputs.outputs: + output_unit = self.visit(node_output) + output_before = output_unit.before_context + output_after = output_unit.after_context + if output_before: + before_context.append(output_before) + if output_after: + after_context.append(output_after) + return DescribeUnit(before_context=before_context, after_context=after_context) + def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: - condition_unit = self._resolve_resource_condition_reference( - node_resource.condition_reference - ) - condition_before = condition_unit.before_context - condition_after = condition_unit.after_context - if not condition_before and condition_after: - change_type = ChangeType.CREATED - elif condition_before and not condition_after: - change_type = ChangeType.REMOVED - else: - change_type = node_resource.change_type - if change_type == ChangeType.UNCHANGED: - # TODO - return None + change_type = node_resource.change_type + if node_resource.condition_reference is not None: + condition_unit = self._resolve_resource_condition_reference( + node_resource.condition_reference + ) + condition_before = condition_unit.before_context + condition_after = condition_unit.after_context + if not condition_before and condition_after: + change_type = ChangeType.CREATED + elif condition_before and not condition_after: + change_type = ChangeType.REMOVED resource_change = cfn_api.ResourceChange() resource_change["LogicalResourceId"] = node_resource.name @@ -432,28 +500,28 @@ def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: ) properties_describe_unit = self.visit(node_resource.properties) - match change_type: - case ChangeType.MODIFIED: - resource_change["Action"] = cfn_api.ChangeAction.Modify - resource_change["BeforeContext"] = properties_describe_unit.before_context - resource_change["AfterContext"] = properties_describe_unit.after_context - case ChangeType.CREATED: - resource_change["Action"] = cfn_api.ChangeAction.Add - resource_change["AfterContext"] = properties_describe_unit.after_context - case ChangeType.REMOVED: - resource_change["Action"] = cfn_api.ChangeAction.Remove - resource_change["BeforeContext"] = properties_describe_unit.before_context - - self._changes.append( - cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change) - ) - # TODO - return None + if change_type != ChangeType.UNCHANGED: + match change_type: + case ChangeType.MODIFIED: + resource_change["Action"] = cfn_api.ChangeAction.Modify + resource_change["BeforeContext"] = properties_describe_unit.before_context + resource_change["AfterContext"] = properties_describe_unit.after_context + case ChangeType.CREATED: + resource_change["Action"] = cfn_api.ChangeAction.Add + resource_change["AfterContext"] = properties_describe_unit.after_context + case ChangeType.REMOVED: + resource_change["Action"] = cfn_api.ChangeAction.Remove + resource_change["BeforeContext"] = properties_describe_unit.before_context + self._changes.append( + cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change) + ) - # def visit_node_resources(self, node_resources: NodeResources) -> DescribeUnit: - # for node_resource in node_resources.resources: - # if node_resource.change_type != ChangeType.UNCHANGED: - # self.visit_node_resource(node_resource=node_resource) - # # TODO - # return None + before_context = None + after_context = None + # TODO: reconsider what is the describe unit return value for a resource type. + if change_type != ChangeType.CREATED: + before_context = node_resource.name + if change_type != ChangeType.REMOVED: + after_context = node_resource.name + return DescribeUnit(before_context=before_context, after_context=after_context) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py index 8a167979fb177..c7340cac44c4b 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py @@ -10,6 +10,8 @@ NodeMapping, NodeMappings, NodeObject, + NodeOutput, + NodeOutputs, NodeParameter, NodeParameters, NodeProperties, @@ -53,6 +55,12 @@ def visit_node_mapping(self, node_mapping: NodeMapping): def visit_node_mappings(self, node_mappings: NodeMappings): self.visit_children(node_mappings) + def visit_node_outputs(self, node_outputs: NodeOutputs): + self.visit_children(node_outputs) + + def visit_node_output(self, node_output: NodeOutput): + self.visit_children(node_output) + def visit_node_parameters(self, node_parameters: NodeParameters): self.visit_children(node_parameters) diff --git a/tests/unit/services/cloudformation/test_change_set_describe_details.py b/tests/unit/services/cloudformation/test_change_set_describe_details.py index d8cf98ff5b757..00df073c977a6 100644 --- a/tests/unit/services/cloudformation/test_change_set_describe_details.py +++ b/tests/unit/services/cloudformation/test_change_set_describe_details.py @@ -10,6 +10,7 @@ ) from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( ChangeSetModelDescriber, + DescribeUnit, ) @@ -36,6 +37,23 @@ def eval_change_set( json_str = json.dumps(changes) return json.loads(json_str) + @staticmethod + def debug_outputs( + before_template: Optional[dict], + after_template: Optional[dict], + before_parameters: Optional[dict] = None, + after_parameters: Optional[dict] = None, + ) -> DescribeUnit: + change_set_model = ChangeSetModel( + before_template=before_template, + after_template=after_template, + before_parameters=before_parameters, + after_parameters=after_parameters, + ) + update_model: NodeTemplate = change_set_model.get_update_model() + outputs_unit = ChangeSetModelDescriber(update_model).visit(update_model.outputs) + return outputs_unit + @staticmethod def compare_changes(computed: list, target: list) -> None: def sort_criteria(resource_change): @@ -517,6 +535,7 @@ def test_parameter_dynamic_change_unrelated_property(self): "Parameter1": { "Type": "AWS::SSM::Parameter", "Properties": { + "Name": "param-name", "Type": "String", "Value": {"Ref": "ParameterValue"}, }, @@ -574,8 +593,12 @@ def test_parameter_dynamic_change_unrelated_property(self): # "ChangeSource": "DirectModification" # } # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, + "BeforeContext": { + "Properties": {"Name": "param-name", "Value": "value-1", "Type": "String"} + }, + "AfterContext": { + "Properties": {"Name": "param-name", "Value": "value-2", "Type": "String"} + }, }, } ] @@ -1454,3 +1477,204 @@ def test_mappings_update_referencing_resource_through_parameter(self): } ] self.compare_changes(changes, target) + + def test_output_new_resource_and_output(self): + t1 = { + "Resources": { + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + } + } + } + t2 = { + "Resources": { + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + "NewParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "param-name", "Type": "String", "Value": "value-1"}, + }, + }, + "Outputs": {"NewParamName": {"Value": {"Ref": "NewParam"}}}, + } + outputs_unit = self.debug_outputs(t1, t2) + assert not outputs_unit.before_context + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.after_context == [{"Name": "NewParamName", "Value": "NewParam"}] + + def test_output_and_resource_removed(self): + t1 = { + "Resources": { + "FeatureToggle": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "app-feature-toggle", + "Type": "String", + "Value": "enabled", + }, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"FeatureToggleName": {"Value": {"Ref": "FeatureToggle"}}}, + } + t2 = { + "Resources": { + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + } + } + } + outputs_unit = self.debug_outputs(t1, t2) + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.before_context == [ + {"Name": "FeatureToggleName", "Value": "FeatureToggle"} + ] + assert outputs_unit.after_context == [] + + def test_output_resource_changed(self): + t1 = { + "Resources": { + "LogLevelParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "app-log-level", "Type": "String", "Value": "info"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"LogLevelOutput": {"Value": {"Ref": "LogLevelParam"}}}, + } + t2 = { + "Resources": { + "LogLevelParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "app-log-level", "Type": "String", "Value": "debug"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"LogLevelOutput": {"Value": {"Ref": "LogLevelParam"}}}, + } + outputs_unit = self.debug_outputs(t1, t2) + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.before_context == [{"Name": "LogLevelOutput", "Value": "LogLevelParam"}] + assert outputs_unit.after_context == [{"Name": "LogLevelOutput", "Value": "LogLevelParam"}] + + def test_output_update(self): + t1 = { + "Resources": { + "EnvParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "app-env", "Type": "String", "Value": "prod"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"EnvParamRef": {"Value": {"Ref": "EnvParam"}}}, + } + + t2 = { + "Resources": { + "EnvParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "app-env", "Type": "String", "Value": "prod"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"EnvParamRef": {"Value": {"Fn::GetAtt": ["EnvParam", "Name"]}}}, + } + outputs_unit = self.debug_outputs(t1, t2) + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.before_context == [{"Name": "EnvParamRef", "Value": "EnvParam"}] + assert outputs_unit.after_context == [ + {"Name": "EnvParamRef", "Value": "{{changeSet:KNOWN_AFTER_APPLY}}"} + ] + + def test_output_renamed(self): + t1 = { + "Resources": { + "SSMParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "some-param", "Type": "String", "Value": "value"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"OldSSMOutput": {"Value": {"Ref": "SSMParam"}}}, + } + t2 = { + "Resources": { + "SSMParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "some-param", "Type": "String", "Value": "value"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"NewSSMOutput": {"Value": {"Ref": "SSMParam"}}}, + } + outputs_unit = self.debug_outputs(t1, t2) + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.before_context == [{"Name": "OldSSMOutput", "Value": "SSMParam"}] + assert outputs_unit.after_context == [{"Name": "NewSSMOutput", "Value": "SSMParam"}] + + def test_output_and_resource_renamed(self): + t1 = { + "Resources": { + "DBPasswordParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "db-password", "Type": "String", "Value": "secret"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"DBPasswordOutput": {"Value": {"Ref": "DBPasswordParam"}}}, + } + t2 = { + "Resources": { + "DatabaseSecretParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "db-password", "Type": "String", "Value": "secret"}, + }, + "UnrelatedParam": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, + }, + }, + "Outputs": {"DatabaseSecretOutput": {"Value": {"Ref": "DatabaseSecretParam"}}}, + } + outputs_unit = self.debug_outputs(t1, t2) + # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, + # as the executor logic is not yet implemented. This will be moved to the template processor. + assert outputs_unit.before_context == [ + {"Name": "DBPasswordOutput", "Value": "DBPasswordParam"} + ] + assert outputs_unit.after_context == [ + {"Name": "DatabaseSecretOutput", "Value": "DatabaseSecretParam"} + ] From 1005e96e6b8bc89ab0d135d6987384d38ca8d24c Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:46:08 +0200 Subject: [PATCH 010/108] Upgrade pinned Python dependencies (#12462) Co-authored-by: LocalStack Bot --- requirements-base-runtime.txt | 6 ++-- requirements-basic.txt | 2 +- requirements-dev.txt | 29 +++++++++------- requirements-runtime.txt | 17 +++++---- requirements-test.txt | 27 ++++++++------- requirements-typehint.txt | 65 ++++++++++++++++++----------------- 6 files changed, 79 insertions(+), 67 deletions(-) diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 233776ada790e..a1dea158e97d1 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -162,11 +162,11 @@ requests-aws4auth==1.3.1 # via localstack-core (pyproject.toml) rfc3339-validator==0.1.4 # via openapi-schema-validator -rich==13.9.4 +rich==14.0.0 # via localstack-core (pyproject.toml) rolo==0.7.5 # via localstack-core (pyproject.toml) -rpds-py==0.23.1 +rpds-py==0.24.0 # via # jsonschema # referencing @@ -180,7 +180,7 @@ six==1.17.0 # rfc3339-validator tailer==0.4.1 # via localstack-core (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.13.0 # via # localstack-twisted # pyopenssl diff --git a/requirements-basic.txt b/requirements-basic.txt index d22dc74524e1d..1dc271fc98481 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -48,7 +48,7 @@ pyyaml==6.0.2 # via localstack-core (pyproject.toml) requests==2.32.3 # via localstack-core (pyproject.toml) -rich==13.9.4 +rich==14.0.0 # via localstack-core (pyproject.toml) semver==3.0.4 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2f2e44af43737..fdb7ffde053b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.2 +amazon-kclpy==3.0.3 # via localstack-core annotated-types==0.7.0 # via pydantic @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.229 +aws-cdk-asset-awscli-v1==2.2.230 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-cloud-assembly-schema==40.7.0 +aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.185.0 +aws-cdk-lib==2.187.0 # via localstack-core aws-sam-translator==1.95.0 # via @@ -68,7 +68,7 @@ cachetools==5.5.2 # airspeed-ext # localstack-core # localstack-core (pyproject.toml) -cattrs==24.1.2 +cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core @@ -82,7 +82,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.0 +cfn-lint==1.32.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -96,7 +96,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.4.2 # via aws-cdk-lib -coverage==7.7.1 +coverage==7.8.0 # via # coveralls # localstack-core @@ -331,9 +331,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.10.6 +pydantic==2.11.1 # via aws-sam-translator -pydantic-core==2.27.2 +pydantic-core==2.33.0 # via pydantic pygments==2.19.1 # via rich @@ -411,13 +411,13 @@ responses==0.25.7 # via moto-ext rfc3339-validator==0.1.4 # via openapi-schema-validator -rich==13.9.4 +rich==14.0.0 # via # localstack-core # localstack-core (pyproject.toml) rolo==0.7.5 # via localstack-core -rpds-py==0.23.1 +rpds-py==0.24.0 # via # jsonschema # referencing @@ -457,7 +457,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.12.2 +typing-extensions==4.13.0 # via # anyio # aws-sam-translator @@ -469,6 +469,9 @@ typing-extensions==4.12.2 # pyopenssl # readerwriterlock # referencing + # typing-inspection +typing-inspection==0.4.0 + # via pydantic urllib3==2.3.0 # via # botocore @@ -477,7 +480,7 @@ urllib3==2.3.0 # opensearch-py # requests # responses -virtualenv==20.29.3 +virtualenv==20.30.0 # via pre-commit websocket-client==1.8.0 # via localstack-core diff --git a/requirements-runtime.txt b/requirements-runtime.txt index c8f16282c01f7..78134753dcea2 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -6,7 +6,7 @@ # airspeed-ext==0.6.7 # via localstack-core (pyproject.toml) -amazon-kclpy==3.0.2 +amazon-kclpy==3.0.3 # via localstack-core (pyproject.toml) annotated-types==0.7.0 # via pydantic @@ -64,7 +64,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.0 +cfn-lint==1.32.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -239,9 +239,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.10.6 +pydantic==2.11.1 # via aws-sam-translator -pydantic-core==2.27.2 +pydantic-core==2.33.0 # via pydantic pygments==2.19.1 # via rich @@ -300,13 +300,13 @@ responses==0.25.7 # via moto-ext rfc3339-validator==0.1.4 # via openapi-schema-validator -rich==13.9.4 +rich==14.0.0 # via # localstack-core # localstack-core (pyproject.toml) rolo==0.7.5 # via localstack-core -rpds-py==0.23.1 +rpds-py==0.24.0 # via # jsonschema # referencing @@ -332,7 +332,7 @@ tailer==0.4.1 # via # localstack-core # localstack-core (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.13.0 # via # aws-sam-translator # cfn-lint @@ -342,6 +342,9 @@ typing-extensions==4.12.2 # pyopenssl # readerwriterlock # referencing + # typing-inspection +typing-inspection==0.4.0 + # via pydantic urllib3==2.3.0 # via # botocore diff --git a/requirements-test.txt b/requirements-test.txt index ca5b4924d67d4..ba2f1a0027a6a 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,7 +6,7 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.2 +amazon-kclpy==3.0.3 # via localstack-core annotated-types==0.7.0 # via pydantic @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.229 +aws-cdk-asset-awscli-v1==2.2.230 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-cloud-assembly-schema==40.7.0 +aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.185.0 +aws-cdk-lib==2.187.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.95.0 # via @@ -68,7 +68,7 @@ cachetools==5.5.2 # airspeed-ext # localstack-core # localstack-core (pyproject.toml) -cattrs==24.1.2 +cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core @@ -80,7 +80,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.0 +cfn-lint==1.32.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -94,7 +94,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.4.2 # via aws-cdk-lib -coverage==7.7.1 +coverage==7.8.0 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core @@ -301,9 +301,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.10.6 +pydantic==2.11.1 # via aws-sam-translator -pydantic-core==2.27.2 +pydantic-core==2.33.0 # via pydantic pygments==2.19.1 # via rich @@ -377,13 +377,13 @@ responses==0.25.7 # via moto-ext rfc3339-validator==0.1.4 # via openapi-schema-validator -rich==13.9.4 +rich==14.0.0 # via # localstack-core # localstack-core (pyproject.toml) rolo==0.7.5 # via localstack-core -rpds-py==0.23.1 +rpds-py==0.24.0 # via # jsonschema # referencing @@ -419,7 +419,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.12.2 +typing-extensions==4.13.0 # via # anyio # aws-sam-translator @@ -431,6 +431,9 @@ typing-extensions==4.12.2 # pyopenssl # readerwriterlock # referencing + # typing-inspection +typing-inspection==0.4.0 + # via pydantic urllib3==2.3.0 # via # botocore diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 88878aeb13889..3be4a0707597e 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -6,7 +6,7 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.2 +amazon-kclpy==3.0.3 # via localstack-core annotated-types==0.7.0 # via pydantic @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.229 +aws-cdk-asset-awscli-v1==2.2.230 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-cloud-assembly-schema==40.7.0 +aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.185.0 +aws-cdk-lib==2.187.0 # via localstack-core aws-sam-translator==1.95.0 # via @@ -51,7 +51,7 @@ boto3==1.37.23 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.37.19 +boto3-stubs==1.37.24 # via localstack-core (pyproject.toml) botocore==1.37.23 # via @@ -61,7 +61,7 @@ botocore==1.37.23 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.37.19 +botocore-stubs==1.37.24 # via boto3-stubs build==1.2.2.post1 # via @@ -72,7 +72,7 @@ cachetools==5.5.2 # airspeed-ext # localstack-core # localstack-core (pyproject.toml) -cattrs==24.1.2 +cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core @@ -86,7 +86,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.0 +cfn-lint==1.32.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -100,7 +100,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.4.2 # via aws-cdk-lib -coverage==7.7.1 +coverage==7.8.0 # via # coveralls # localstack-core @@ -266,9 +266,9 @@ mypy-boto3-acm-pca==1.37.12 # via boto3-stubs mypy-boto3-amplify==1.37.17 # via boto3-stubs -mypy-boto3-apigateway==1.37.0 +mypy-boto3-apigateway==1.37.23 # via boto3-stubs -mypy-boto3-apigatewayv2==1.37.0 +mypy-boto3-apigatewayv2==1.37.23 # via boto3-stubs mypy-boto3-appconfig==1.37.0 # via boto3-stubs @@ -284,13 +284,13 @@ mypy-boto3-autoscaling==1.37.0 # via boto3-stubs mypy-boto3-backup==1.37.0 # via boto3-stubs -mypy-boto3-batch==1.37.2 +mypy-boto3-batch==1.37.22 # via boto3-stubs mypy-boto3-ce==1.37.10 # via boto3-stubs mypy-boto3-cloudcontrol==1.37.0 # via boto3-stubs -mypy-boto3-cloudformation==1.37.0 +mypy-boto3-cloudformation==1.37.22 # via boto3-stubs mypy-boto3-cloudfront==1.37.9 # via boto3-stubs @@ -298,7 +298,7 @@ mypy-boto3-cloudtrail==1.37.8 # via boto3-stubs mypy-boto3-cloudwatch==1.37.0 # via boto3-stubs -mypy-boto3-codebuild==1.37.12 +mypy-boto3-codebuild==1.37.23 # via boto3-stubs mypy-boto3-codecommit==1.37.0 # via boto3-stubs @@ -320,15 +320,15 @@ mypy-boto3-dynamodb==1.37.12 # via boto3-stubs mypy-boto3-dynamodbstreams==1.37.0 # via boto3-stubs -mypy-boto3-ec2==1.37.16 +mypy-boto3-ec2==1.37.24 # via boto3-stubs mypy-boto3-ecr==1.37.11 # via boto3-stubs -mypy-boto3-ecs==1.37.11 +mypy-boto3-ecs==1.37.23 # via boto3-stubs mypy-boto3-efs==1.37.0 # via boto3-stubs -mypy-boto3-eks==1.37.4 +mypy-boto3-eks==1.37.24 # via boto3-stubs mypy-boto3-elasticache==1.37.6 # via boto3-stubs @@ -352,7 +352,7 @@ mypy-boto3-glacier==1.37.0 # via boto3-stubs mypy-boto3-glue==1.37.13 # via boto3-stubs -mypy-boto3-iam==1.37.0 +mypy-boto3-iam==1.37.22 # via boto3-stubs mypy-boto3-identitystore==1.37.0 # via boto3-stubs @@ -382,7 +382,7 @@ mypy-boto3-logs==1.37.12 # via boto3-stubs mypy-boto3-managedblockchain==1.37.0 # via boto3-stubs -mypy-boto3-mediaconvert==1.37.15 +mypy-boto3-mediaconvert==1.37.21 # via boto3-stubs mypy-boto3-mediastore==1.37.0 # via boto3-stubs @@ -406,7 +406,7 @@ mypy-boto3-qldb==1.37.0 # via boto3-stubs mypy-boto3-qldb-session==1.37.0 # via boto3-stubs -mypy-boto3-rds==1.37.6 +mypy-boto3-rds==1.37.21 # via boto3-stubs mypy-boto3-rds-data==1.37.0 # via boto3-stubs @@ -422,11 +422,11 @@ mypy-boto3-route53==1.37.15 # via boto3-stubs mypy-boto3-route53resolver==1.37.0 # via boto3-stubs -mypy-boto3-s3==1.37.0 +mypy-boto3-s3==1.37.24 # via boto3-stubs -mypy-boto3-s3control==1.37.12 +mypy-boto3-s3control==1.37.24 # via boto3-stubs -mypy-boto3-sagemaker==1.37.18 +mypy-boto3-sagemaker==1.37.23 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.37.0 # via boto3-stubs @@ -438,7 +438,7 @@ mypy-boto3-servicediscovery==1.37.0 # via boto3-stubs mypy-boto3-ses==1.37.0 # via boto3-stubs -mypy-boto3-sesv2==1.37.0 +mypy-boto3-sesv2==1.37.24 # via boto3-stubs mypy-boto3-sns==1.37.0 # via boto3-stubs @@ -458,7 +458,7 @@ mypy-boto3-timestream-write==1.37.0 # via boto3-stubs mypy-boto3-transcribe==1.37.5 # via boto3-stubs -mypy-boto3-wafv2==1.37.14 +mypy-boto3-wafv2==1.37.21 # via boto3-stubs mypy-boto3-xray==1.37.0 # via boto3-stubs @@ -537,9 +537,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.10.6 +pydantic==2.11.1 # via aws-sam-translator -pydantic-core==2.27.2 +pydantic-core==2.33.0 # via pydantic pygments==2.19.1 # via rich @@ -617,13 +617,13 @@ responses==0.25.7 # via moto-ext rfc3339-validator==0.1.4 # via openapi-schema-validator -rich==13.9.4 +rich==14.0.0 # via # localstack-core # localstack-core (pyproject.toml) rolo==0.7.5 # via localstack-core -rpds-py==0.23.1 +rpds-py==0.24.0 # via # jsonschema # referencing @@ -667,7 +667,7 @@ types-awscrt==0.24.2 # via botocore-stubs types-s3transfer==0.11.4 # via boto3-stubs -typing-extensions==4.12.2 +typing-extensions==4.13.0 # via # anyio # aws-sam-translator @@ -781,6 +781,9 @@ typing-extensions==4.12.2 # pyopenssl # readerwriterlock # referencing + # typing-inspection +typing-inspection==0.4.0 + # via pydantic urllib3==2.3.0 # via # botocore @@ -789,7 +792,7 @@ urllib3==2.3.0 # opensearch-py # requests # responses -virtualenv==20.29.3 +virtualenv==20.30.0 # via pre-commit websocket-client==1.8.0 # via localstack-core From ac0ff24e4d1f48267e62ab1cd37fd4758638819a Mon Sep 17 00:00:00 2001 From: Bernhard Matyas <90144234+baermat@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:38:57 +0200 Subject: [PATCH 011/108] Deleting a FIFO message with an expired receipt handle should raise an error (#12442) --- .../localstack/services/sqs/models.py | 25 +++++- .../localstack/services/sqs/utils.py | 23 +++-- tests/aws/services/sqs/test_sqs.py | 52 +++++++++++- tests/aws/services/sqs/test_sqs.snapshot.json | 84 +++++++++++++++++++ .../aws/services/sqs/test_sqs.validation.json | 14 +++- 5 files changed, 186 insertions(+), 12 deletions(-) diff --git a/localstack-core/localstack/services/sqs/models.py b/localstack-core/localstack/services/sqs/models.py index 779a95437ad91..8e7352bd28172 100644 --- a/localstack-core/localstack/services/sqs/models.py +++ b/localstack-core/localstack/services/sqs/models.py @@ -30,9 +30,9 @@ ) from localstack.services.sqs.queue import InterruptiblePriorityQueue, InterruptibleQueue from localstack.services.sqs.utils import ( - decode_receipt_handle, encode_move_task_handle, encode_receipt_handle, + extract_receipt_handle_info, global_message_sequence, guess_endpoint_strategy_and_host, is_message_deduplication_id_required, @@ -445,7 +445,7 @@ def approx_number_of_messages_delayed(self) -> int: return len(self.delayed) def validate_receipt_handle(self, receipt_handle: str): - if self.arn != decode_receipt_handle(receipt_handle): + if self.arn != extract_receipt_handle_info(receipt_handle).queue_arn: raise ReceiptHandleIsInvalid( f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.' ) @@ -490,6 +490,7 @@ def remove(self, receipt_handle: str): return standard_message = self.receipts[receipt_handle] + self._pre_delete_checks(standard_message, receipt_handle) standard_message.deleted = True LOG.debug( "deleting message %s from queue %s", @@ -724,6 +725,18 @@ def remove_expired_messages_from_heap( return expired + def _pre_delete_checks(self, standard_message: SqsMessage, receipt_handle: str) -> None: + """ + Runs any potential checks if a message that has been successfully identified via a receipt handle + is indeed supposed to be deleted. + For example, a receipt handle that has expired might not lead to deletion. + + :param standard_message: The message to be deleted + :param receipt_handle: The handle associated with the message + :return: None. Potential violations raise errors. + """ + pass + class StandardQueue(SqsQueue): visible: InterruptiblePriorityQueue[SqsMessage] @@ -1001,9 +1014,15 @@ def update_delay_seconds(self, value: int): for message in self.delayed: message.delay_seconds = value + def _pre_delete_checks(self, message: SqsMessage, receipt_handle: str) -> None: + _, _, _, last_received = extract_receipt_handle_info(receipt_handle) + if time.time() - float(last_received) > message.visibility_timeout: + raise InvalidParameterValueException( + f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired." + ) + def remove(self, receipt_handle: str): self.validate_receipt_handle(receipt_handle) - decode_receipt_handle(receipt_handle) super().remove(receipt_handle) diff --git a/localstack-core/localstack/services/sqs/utils.py b/localstack-core/localstack/services/sqs/utils.py index 70d5876454759..50341e04d7de1 100644 --- a/localstack-core/localstack/services/sqs/utils.py +++ b/localstack-core/localstack/services/sqs/utils.py @@ -3,7 +3,7 @@ import json import re import time -from typing import Literal, Optional, Tuple +from typing import Literal, NamedTuple, Optional, Tuple from urllib.parse import urlparse from localstack.aws.api.sqs import QueueAttributeName, ReceiptHandleIsInvalid @@ -116,16 +116,25 @@ def parse_queue_url(queue_url: str) -> Tuple[str, Optional[str], str]: return account_id, region, queue_name -def decode_receipt_handle(receipt_handle: str) -> str: +class ReceiptHandleInformation(NamedTuple): + identifier: str + queue_arn: str + message_id: str + last_received: str + + +def extract_receipt_handle_info(receipt_handle: str) -> ReceiptHandleInformation: try: handle = base64.b64decode(receipt_handle).decode("utf-8") - _, queue_arn, message_id, last_received = handle.split(" ") - parse_arn(queue_arn) # raises a ValueError if it is not an arn - return queue_arn - except (IndexError, ValueError): + parts = handle.split(" ") + if len(parts) != 4: + raise ValueError(f'The input receipt handle "{receipt_handle}" is incomplete.') + parse_arn(parts[1]) + return ReceiptHandleInformation(*parts) + except (IndexError, ValueError) as e: raise ReceiptHandleIsInvalid( f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.' - ) + ) from e def encode_receipt_handle(queue_arn, message) -> str: diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index d34055e4180d0..9a562517da77a 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -16,7 +16,10 @@ from localstack.services.sqs.constants import DEFAULT_MAXIMUM_MESSAGE_SIZE, SQS_UUID_STRING_SEED from localstack.services.sqs.models import sqs_stores from localstack.services.sqs.provider import MAX_NUMBER_OF_MESSAGES -from localstack.services.sqs.utils import parse_queue_url, token_generator +from localstack.services.sqs.utils import ( + parse_queue_url, + token_generator, +) from localstack.testing.aws.util import is_aws_cloud from localstack.testing.config import ( SECONDARY_TEST_AWS_ACCESS_KEY_ID, @@ -1103,6 +1106,53 @@ def test_receive_after_visibility_timeout(self, sqs_create_queue, aws_sqs_client "receipt handles should be different" ) + @markers.aws.validated + def test_delete_after_visibility_timeout(self, sqs_create_queue, aws_sqs_client, snapshot): + timeout = 1 + queue_url = sqs_create_queue( + QueueName=f"test-{short_uid()}", Attributes={"VisibilityTimeout": f"{timeout}"} + ) + + aws_sqs_client.send_message(QueueUrl=queue_url, MessageBody="foobar") + # receive the message + initial_receive = aws_sqs_client.receive_message(QueueUrl=queue_url, WaitTimeSeconds=5) + assert "Messages" in initial_receive + receipt_handle = initial_receive["Messages"][0]["ReceiptHandle"] + + # exceed the visibility timeout window + time.sleep(timeout) + + aws_sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) + + snapshot.match( + "delete_after_timeout_queue_empty", aws_sqs_client.receive_message(QueueUrl=queue_url) + ) + + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) + @markers.aws.validated + def test_fifo_delete_after_visibility_timeout(self, sqs_create_queue, aws_sqs_client, snapshot): + timeout = 1 + queue_url = sqs_create_queue( + QueueName=f"test-{short_uid()}.fifo", + Attributes={ + "VisibilityTimeout": f"{timeout}", + "FifoQueue": "True", + "ContentBasedDeduplication": "True", + }, + ) + + aws_sqs_client.send_message(QueueUrl=queue_url, MessageBody="foobar", MessageGroupId="1") + # receive the message + initial_receive = aws_sqs_client.receive_message(QueueUrl=queue_url, WaitTimeSeconds=5) + snapshot.match("received_sqs_message", initial_receive) + receipt_handle = initial_receive["Messages"][0]["ReceiptHandle"] + + # exceed the visibility timeout window + time.sleep(timeout) + with pytest.raises(ClientError) as e: + aws_sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) + snapshot.match("delete_after_timeout_fifo", e.value.response) + @markers.aws.validated def test_receive_terminate_visibility_timeout(self, sqs_queue, aws_sqs_client): queue_url = sqs_queue diff --git a/tests/aws/services/sqs/test_sqs.snapshot.json b/tests/aws/services/sqs/test_sqs.snapshot.json index 339e904a5dc15..3f98e7ec95cd8 100644 --- a/tests/aws/services/sqs/test_sqs.snapshot.json +++ b/tests/aws/services/sqs/test_sqs.snapshot.json @@ -3890,5 +3890,89 @@ } } } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_after_visibility_timeout[sqs]": { + "recorded-date": "28-03-2025, 13:46:28", + "recorded-content": { + "delete_after_timeout_queue_empty": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_after_visibility_timeout[sqs_query]": { + "recorded-date": "28-03-2025, 13:46:31", + "recorded-content": { + "delete_after_timeout_queue_empty": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_after_visibility_timeout[sqs]": { + "recorded-date": "28-03-2025, 13:28:19", + "recorded-content": { + "received_sqs_message": { + "Messages": [ + { + "Body": "foobar", + "MD5OfBody": "3858f62230ac3c915f300c664312c63f", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete_after_timeout_fifo": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "Value for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired.", + "QueryErrorCode": "InvalidParameterValueException", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_after_visibility_timeout[sqs_query]": { + "recorded-date": "28-03-2025, 13:28:23", + "recorded-content": { + "received_sqs_message": { + "Messages": [ + { + "Body": "foobar", + "MD5OfBody": "3858f62230ac3c915f300c664312c63f", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete_after_timeout_fifo": { + "Error": { + "Code": "InvalidParameterValue", + "Detail": null, + "Message": "Value for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/sqs/test_sqs.validation.json b/tests/aws/services/sqs/test_sqs.validation.json index b644f91f59d7d..f8dba2d87c978 100644 --- a/tests/aws/services/sqs/test_sqs.validation.json +++ b/tests/aws/services/sqs/test_sqs.validation.json @@ -68,6 +68,12 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_deduplication_interval[sqs_query]": { "last_validated_date": "2024-05-29T13:47:36+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_after_visibility_timeout[sqs]": { + "last_validated_date": "2025-03-28T13:46:27+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_after_visibility_timeout[sqs_query]": { + "last_validated_date": "2025-03-28T13:46:31+00:00" + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-]": { "last_validated_date": "2024-04-30T13:48:34+00:00" }, @@ -128,6 +134,12 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-True]": { "last_validated_date": "2024-04-30T13:34:17+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_after_visibility_timeout[sqs]": { + "last_validated_date": "2025-03-28T13:37:10+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_after_visibility_timeout[sqs_query]": { + "last_validated_date": "2025-03-28T13:37:13+00:00" + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs]": { "last_validated_date": "2024-04-30T13:46:32+00:00" }, @@ -425,4 +437,4 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_send_message_via_queue_url_with_json_protocol": { "last_validated_date": "2024-04-30T13:35:11+00:00" } -} \ No newline at end of file +} From a6be285d062ba12dc9c3660980d35bfa86b66ca3 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:08:07 +0200 Subject: [PATCH 012/108] S3: fix casing of PreSignedPost validation (#12449) --- .../localstack/services/s3/presigned_url.py | 26 +++++----- tests/aws/services/s3/test_s3.py | 49 +++++++++++++++++++ tests/aws/services/s3/test_s3.validation.json | 6 +++ 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/localstack-core/localstack/services/s3/presigned_url.py b/localstack-core/localstack/services/s3/presigned_url.py index 573ac0a257a0a..ecdd527e65861 100644 --- a/localstack-core/localstack/services/s3/presigned_url.py +++ b/localstack-core/localstack/services/s3/presigned_url.py @@ -60,7 +60,7 @@ SIGNATURE_V2_POST_FIELDS = [ "signature", - "AWSAccessKeyId", + "awsaccesskeyid", ] SIGNATURE_V4_POST_FIELDS = [ @@ -768,13 +768,17 @@ def validate_post_policy( ) raise ex - if not (policy := request_form.get("policy")): + form_dict = {k.lower(): v for k, v in request_form.items()} + + policy = form_dict.get("policy") + if not policy: # A POST request needs a policy except if the bucket is publicly writable return # TODO: this does validation of fields only for now - is_v4 = _is_match_with_signature_fields(request_form, SIGNATURE_V4_POST_FIELDS) - is_v2 = _is_match_with_signature_fields(request_form, SIGNATURE_V2_POST_FIELDS) + is_v4 = _is_match_with_signature_fields(form_dict, SIGNATURE_V4_POST_FIELDS) + is_v2 = _is_match_with_signature_fields(form_dict, SIGNATURE_V2_POST_FIELDS) + if not is_v2 and not is_v4: ex: AccessDenied = AccessDenied("Access Denied") ex.HostId = FAKE_HOST_ID @@ -784,7 +788,7 @@ def validate_post_policy( policy_decoded = json.loads(base64.b64decode(policy).decode("utf-8")) except ValueError: # this means the policy has been tampered with - signature = request_form.get("signature") if is_v2 else request_form.get("x-amz-signature") + signature = form_dict.get("signature") if is_v2 else form_dict.get("x-amz-signature") credentials = get_credentials_from_parameters(request_form, "us-east-1") ex: SignatureDoesNotMatch = create_signature_does_not_match_sig_v2( request_signature=signature, @@ -813,7 +817,6 @@ def validate_post_policy( return conditions = policy_decoded.get("conditions", []) - form_dict = {k.lower(): v for k, v in request_form.items()} for condition in conditions: if not _verify_condition(condition, form_dict, additional_policy_metadata): str_condition = str(condition).replace("'", '"') @@ -896,7 +899,7 @@ def _parse_policy_expiration_date(expiration_string: str) -> datetime.datetime: def _is_match_with_signature_fields( - request_form: ImmutableMultiDict, signature_fields: list[str] + request_form: dict[str, str], signature_fields: list[str] ) -> bool: """ Checks if the form contains at least one of the required fields passed in `signature_fields` @@ -910,12 +913,13 @@ def _is_match_with_signature_fields( for p in signature_fields: if p not in request_form: LOG.info("POST pre-sign missing fields") - # .capitalize() does not work here, because of AWSAccessKeyId casing argument_name = ( - capitalize_header_name_from_snake_case(p) - if "-" in p - else f"{p[0].upper()}{p[1:]}" + capitalize_header_name_from_snake_case(p) if "-" in p else p.capitalize() ) + # AWSAccessKeyId is a special case + if argument_name == "Awsaccesskeyid": + argument_name = "AWSAccessKeyId" + ex: InvalidArgument = _create_invalid_argument_exc( message=f"Bucket POST must contain a field named '{argument_name}'. If it is specified, please check the order of the fields.", name=argument_name, diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index 585c6b166d983..5a0dc6dca7abd 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -11150,6 +11150,55 @@ def test_presigned_post_with_different_user_credentials( get_obj = aws_client.s3.get_object(Bucket=bucket_name, Key=object_key) snapshot.match("get-obj", get_obj) + @markers.aws.validated + @pytest.mark.parametrize( + "signature_version", + ["s3", "s3v4"], + ) + def test_post_object_policy_casing(self, s3_bucket, signature_version): + object_key = "validate-policy-casing" + presigned_client = _s3_client_pre_signed_client( + Config(signature_version=signature_version), + endpoint_url=_endpoint_url(), + ) + presigned_request = presigned_client.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + ExpiresIn=60, + Conditions=[ + {"bucket": s3_bucket}, + ["content-length-range", 5, 10], + ], + ) + + # test that we can change the casing of the Policy field + fields = presigned_request["fields"] + fields["Policy"] = fields.pop("policy") + response = requests.post( + presigned_request["url"], + data=fields, + files={"file": "a" * 5}, + verify=False, + ) + assert response.status_code == 204 + + # test that we can change the casing of the credentials field + if signature_version == "s3": + field_name = "AWSAccessKeyId" + new_field_name = "awsaccesskeyid" + else: + field_name = "x-amz-credential" + new_field_name = "X-Amz-Credential" + + fields[new_field_name] = fields.pop(field_name) + response = requests.post( + presigned_request["url"], + data=fields, + files={"file": "a" * 5}, + verify=False, + ) + assert response.status_code == 204 + # LocalStack does not apply encryption, so the ETag is different @markers.snapshot.skip_snapshot_verify(paths=["$..ETag"]) diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index 4f87e6e01dae9..d26a2cce6ff21 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -710,6 +710,12 @@ "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_default_checksum": { "last_validated_date": "2025-03-17T21:46:24+00:00" }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_casing[s3]": { + "last_validated_date": "2025-03-28T19:11:34+00:00" + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_casing[s3v4]": { + "last_validated_date": "2025-03-28T19:11:36+00:00" + }, "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": { "last_validated_date": "2025-03-17T20:16:55+00:00" }, From 9383d5084d708330f1cdfadb7561cba65a12d1a9 Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Tue, 1 Apr 2025 17:48:31 +0200 Subject: [PATCH 013/108] Lambda: Added Ruby 3.4 Runtime (#12458) --- .../localstack/services/lambda_/provider.py | 6 + .../localstack/services/lambda_/runtimes.py | 17 +- tests/aws/services/lambda_/test_lambda_api.py | 9 +- .../lambda_/test_lambda_api.snapshot.json | 3781 +---------------- .../lambda_/test_lambda_api.validation.json | 139 +- .../lambda_/test_lambda_common.snapshot.json | 460 +- .../test_lambda_common.validation.json | 207 +- 7 files changed, 736 insertions(+), 3883 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index 9356dde0f281c..54a21f9047d9c 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -223,6 +223,7 @@ DEPRECATED_RUNTIMES, DEPRECATED_RUNTIMES_UPGRADES, RUNTIMES_AGGREGATED, + SNAP_START_SUPPORTED_RUNTIMES, VALID_RUNTIMES, ) from localstack.services.lambda_.urlrouter import FunctionUrlRouter @@ -718,6 +719,11 @@ def _validate_snapstart(snap_start: SnapStart, runtime: Runtime): f"1 validation error detected: Value '{apply_on}' at 'snapStart.applyOn' failed to satisfy constraint: Member must satisfy enum value set: [PublishedVersions, None]" ) + if runtime not in SNAP_START_SUPPORTED_RUNTIMES: + raise InvalidParameterValueException( + f"{runtime} is not supported for SnapStart enabled functions.", Type="User" + ) + def _validate_layers(self, new_layers: list[str], region: str, account_id: str): if len(new_layers) > LAMBDA_LAYERS_LIMIT_PER_FUNCTION: raise InvalidParameterValueException( diff --git a/localstack-core/localstack/services/lambda_/runtimes.py b/localstack-core/localstack/services/lambda_/runtimes.py index 4eaf2a876f04e..3fa96216257f6 100644 --- a/localstack-core/localstack/services/lambda_/runtimes.py +++ b/localstack-core/localstack/services/lambda_/runtimes.py @@ -59,6 +59,7 @@ Runtime.dotnet6: "dotnet:6", Runtime.dotnetcore3_1: "dotnet:core3.1", # deprecated Apr 3, 2023 => Apr 3, 2023 => May 3, 2023 Runtime.go1_x: "go:1", # deprecated Jan 8, 2024 => Feb 8, 2024 => Mar 12, 2024 + Runtime.ruby3_4: "ruby:3.4", Runtime.ruby3_3: "ruby:3.3", Runtime.ruby3_2: "ruby:3.2", Runtime.ruby2_7: "ruby:2.7", # deprecated Dec 7, 2023 => Jan 9, 2024 => Feb 8, 2024 @@ -133,6 +134,7 @@ "ruby": [ Runtime.ruby3_2, Runtime.ruby3_3, + Runtime.ruby3_4, ], "dotnet": [ Runtime.dotnet6, @@ -149,7 +151,18 @@ runtime for runtime_group in RUNTIMES_AGGREGATED.values() for runtime in runtime_group ] +# An unordered list of snapstart-enabled runtimes. Related to snapshots in test_snapstart_exceptions +# https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html +SNAP_START_SUPPORTED_RUNTIMES = [ + Runtime.java11, + Runtime.java17, + Runtime.java21, + Runtime.python3_12, + Runtime.python3_13, + Runtime.dotnet8, +] + # An ordered list of all Lambda runtimes considered valid by AWS. Matching snapshots in test_create_lambda_exceptions -VALID_RUNTIMES: str = "[nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]" +VALID_RUNTIMES: str = "[nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9]" # An ordered list of all Lambda runtimes for layers considered valid by AWS. Matching snapshots in test_layer_exceptions -VALID_LAYER_RUNTIMES: str = "[ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java17, nodejs, nodejs4.3, java8.al2, go1.x, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, nodejs22.x, python3.10, java8, nodejs12.x, python3.11, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, python3.13, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby2.5, python3.6, python2.7]" +VALID_LAYER_RUNTIMES: str = "[ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java25, java17, nodejs, nodejs4.3, java8.al2, go1.x, dotnet10, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, nodejs22.x, python3.10, java8, nodejs12.x, python3.11, nodejs24.x, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, python3.13, python3.14, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby3.4, ruby2.5, python3.6, python2.7]" diff --git a/tests/aws/services/lambda_/test_lambda_api.py b/tests/aws/services/lambda_/test_lambda_api.py index c81517b2d375b..9b897a1326192 100644 --- a/tests/aws/services/lambda_/test_lambda_api.py +++ b/tests/aws/services/lambda_/test_lambda_api.py @@ -38,6 +38,7 @@ from localstack.services.lambda_.runtimes import ( ALL_RUNTIMES, DEPRECATED_RUNTIMES, + SNAP_START_SUPPORTED_RUNTIMES, ) from localstack.testing.aws.lambda_utils import ( _await_dynamodb_table_active, @@ -6827,14 +6828,12 @@ def test_layer_deterministic_version( class TestLambdaSnapStart: @markers.aws.validated @markers.lambda_runtime_update - @markers.multiruntime(scenario="echo") + @markers.multiruntime(scenario="echo", runtimes=SNAP_START_SUPPORTED_RUNTIMES) def test_snapstart_lifecycle(self, multiruntime_lambda, snapshot, aws_client): """Test the API of the SnapStart feature. The optimization behavior is not supported in LocalStack. Slow (~1-2min) against AWS. """ - create_function_response = multiruntime_lambda.create_function( - MemorySize=1024, Timeout=5, SnapStart={"ApplyOn": "PublishedVersions"} - ) + create_function_response = multiruntime_lambda.create_function(MemorySize=1024, Timeout=5) function_name = create_function_response["FunctionName"] snapshot.match("create_function_response", create_function_response) @@ -6856,7 +6855,7 @@ def test_snapstart_lifecycle(self, multiruntime_lambda, snapshot, aws_client): @markers.aws.validated @markers.lambda_runtime_update - @markers.multiruntime(scenario="echo") + @markers.multiruntime(scenario="echo", runtimes=SNAP_START_SUPPORTED_RUNTIMES) def test_snapstart_update_function_configuration( self, multiruntime_lambda, snapshot, aws_client ): diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index 773873420f03d..1e63ff2f5b8b0 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -7848,7 +7848,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_create_lambda_exceptions": { - "recorded-date": "26-11-2024, 09:27:31", + "recorded-date": "01-04-2025, 13:08:21", "recorded-content": { "invalid_role_arn_exc": { "Error": { @@ -7863,10 +7863,10 @@ "invalid_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7875,10 +7875,10 @@ "uppercase_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7920,7 +7920,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_update_lambda_exceptions": { - "recorded-date": "26-11-2024, 09:27:33", + "recorded-date": "01-04-2025, 13:09:51", "recorded-content": { "invalid_role_arn_exc": { "Error": { @@ -7935,10 +7935,10 @@ "invalid_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7947,10 +7947,10 @@ "uppercase_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -8260,7 +8260,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": { - "recorded-date": "26-11-2024, 09:27:46", + "recorded-date": "01-04-2025, 13:19:20", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -8287,7 +8287,7 @@ "list_layers_exc_compatibleruntime_invalid": { "Error": { "Code": "ValidationException", - "Message": "1 validation error detected: Value 'runtimedoesnotexist' at 'compatibleRuntime' failed to satisfy constraint: Member must satisfy enum value set: [ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java17, nodejs, nodejs4.3, java8.al2, go1.x, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, nodejs22.x, python3.10, java8, nodejs12.x, python3.11, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, python3.13, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby2.5, python3.6, python2.7]" + "Message": "1 validation error detected: Value 'runtimedoesnotexist' at 'compatibleRuntime' failed to satisfy constraint: Member must satisfy enum value set: [ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java25, java17, nodejs, nodejs4.3, java8.al2, go1.x, dotnet10, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, nodejs22.x, python3.10, java8, nodejs12.x, python3.11, nodejs24.x, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, python3.13, python3.14, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby3.4, ruby2.5, python3.6, python2.7]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -8438,7 +8438,7 @@ "publish_layer_version_exc_invalid_runtime_arch": { "Error": { "Code": "ValidationException", - "Message": "2 validation errors detected: Value '[invalidruntime]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" + "Message": "2 validation errors detected: Value '[invalidruntime]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -8448,7 +8448,7 @@ "publish_layer_version_exc_partially_invalid_values": { "Error": { "Code": "ValidationException", - "Message": "2 validation errors detected: Value '[invalidruntime, invalidruntime2, nodejs20.x]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch, x86_64]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" + "Message": "2 validation errors detected: Value '[invalidruntime, invalidruntime2, nodejs20.x]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, python3.13, nodejs22.x, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, ruby3.4, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch, x86_64]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -12407,7 +12407,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_exceptions": { - "recorded-date": "09-12-2024, 15:23:03", + "recorded-date": "31-03-2025, 16:15:53", "recorded-content": { "create_function_invalid_snapstart_apply": { "Error": { @@ -12881,7 +12881,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java11]": { - "recorded-date": "09-12-2024, 15:03:12", + "recorded-date": "01-04-2025, 13:30:54", "recorded-content": { "create_function_response": { "Architectures": [ @@ -12910,7 +12910,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -12959,7 +12959,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -13007,8 +13007,8 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -13025,7 +13025,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java17]": { - "recorded-date": "09-12-2024, 15:01:48", + "recorded-date": "01-04-2025, 13:30:58", "recorded-content": { "create_function_response": { "Architectures": [ @@ -13054,7 +13054,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -13103,7 +13103,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -13151,8 +13151,8 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -13168,190 +13168,6 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java11]": { - "recorded-date": "09-12-2024, 15:28:21", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": { - "recorded-date": "09-12-2024, 15:28:18", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java17", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java17", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_vpc_config": { "recorded-date": "12-09-2024, 11:34:43", "recorded-content": { @@ -13705,49 +13521,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities": { - "recorded-date": "30-08-2023, 09:57:12", - "recorded-content": { - "publish_result": { - "CompatibleArchitectures": [ - "arm64", - "x86_64" - ], - "CompatibleRuntimes": [ - "nodejs12.x", - "nodejs14.x", - "nodejs16.x", - "nodejs18.x", - "python3.7", - "python3.8", - "python3.9", - "python3.10", - "python3.11", - "ruby2.7", - "ruby3.2", - "java8", - "java8.al2", - "java11" - ], - "Content": { - "CodeSha256": "", - "CodeSize": "", - "Location": "" - }, - "CreatedDate": "date", - "Description": "", - "LayerArn": "arn::lambda::111111111111:layer:", - "LayerVersionArn": "arn::lambda::111111111111:layer::1", - "Version": 1, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - } - } - }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "recorded-date": "26-11-2024, 09:27:34", + "recorded-date": "01-04-2025, 13:12:59", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13788,7 +13563,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "recorded-date": "26-11-2024, 09:27:38", + "recorded-date": "01-04-2025, 13:13:03", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13804,6 +13579,7 @@ "dotnet6", "dotnetcore3.1", "go1.x", + "ruby3.4", "ruby3.3", "ruby3.2", "ruby2.7", @@ -13883,7 +13659,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java21]": { - "recorded-date": "09-12-2024, 15:00:20", + "recorded-date": "01-04-2025, 13:31:02", "recorded-content": { "create_function_response": { "Architectures": [ @@ -13912,7 +13688,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -13961,7 +13737,7 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -14009,8 +13785,8 @@ "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -14462,10 +14238,10 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": { - "recorded-date": "09-12-2024, 15:28:16", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { + "recorded-date": "12-09-2024, 11:34:47", "recorded-content": { - "create_function_response": { + "create-function-response": { "Architectures": [ "x86_64" ], @@ -14477,17 +14253,17 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "echo.Handler", + "Handler": "handler.handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", "LogGroup": "/aws/lambda/" }, - "MemorySize": 1024, + "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "java21", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -14498,99 +14274,7 @@ "State": "Pending", "StateReason": "The function is being created.", "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java21", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { - "recorded-date": "12-09-2024, 11:34:47", - "recorded-content": { - "create-function-response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 128, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 3, + "Timeout": 3, "TracingConfig": { "Mode": "PassThrough" }, @@ -16254,25 +15938,8 @@ "recorded-date": "05-06-2024, 11:49:05", "recorded-content": {} }, - "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled": { - "recorded-date": "12-06-2024, 14:19:11", - "recorded-content": { - "deprecation_error": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "The runtime parameter of python3.7 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (python3.12) while creating or updating functions." - }, - "Type": "User", - "message": "The runtime parameter of python3.7 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (python3.12) while creating or updating functions.", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[java8]": { - "recorded-date": "26-11-2024, 09:27:29", + "recorded-date": "01-04-2025, 13:02:29", "recorded-content": { "deprecation_error": { "Error": { @@ -16289,7 +15956,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[go1.x]": { - "recorded-date": "26-11-2024, 09:27:29", + "recorded-date": "01-04-2025, 13:02:29", "recorded-content": { "deprecation_error": { "Error": { @@ -16306,7 +15973,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[provided]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:30", "recorded-content": { "deprecation_error": { "Error": { @@ -16323,7 +15990,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[ruby2.7]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:30", "recorded-content": { "deprecation_error": { "Error": { @@ -16340,7 +16007,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[nodejs14.x]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:30", "recorded-content": { "deprecation_error": { "Error": { @@ -16357,7 +16024,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[python3.7]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:30", "recorded-content": { "deprecation_error": { "Error": { @@ -16374,7 +16041,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[dotnetcore3.1]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:31", "recorded-content": { "deprecation_error": { "Error": { @@ -16391,7 +16058,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[nodejs12.x]": { - "recorded-date": "26-11-2024, 09:27:30", + "recorded-date": "01-04-2025, 13:02:31", "recorded-content": { "deprecation_error": { "Error": { @@ -18269,8 +17936,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs22.x]": { - "recorded-date": "09-12-2024, 14:45:02", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.13]": { + "recorded-date": "01-04-2025, 13:31:11", "recorded-content": { "create_function_response": { "Architectures": [ @@ -18284,7 +17951,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -18294,12 +17961,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs22.x", + "Runtime": "python3.13", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -18332,7 +17999,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18343,12 +18010,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs22.x", + "Runtime": "python3.13", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -18380,7 +18047,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function::1", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18391,13 +18058,13 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs22.x", + "Runtime": "python3.13", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -18413,8 +18080,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs20.x]": { - "recorded-date": "09-12-2024, 14:46:41", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.12]": { + "recorded-date": "01-04-2025, 13:31:07", "recorded-content": { "create_function_response": { "Architectures": [ @@ -18428,7 +18095,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -18438,12 +18105,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs20.x", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -18476,7 +18143,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18487,12 +18154,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs20.x", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -18524,7 +18191,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function::1", "FunctionName": "", - "Handler": "index.handler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18535,13 +18202,13 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs20.x", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -18557,8 +18224,89 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs18.x]": { - "recorded-date": "09-12-2024, 14:48:09", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_vpc_config_subnet": { + "recorded-date": "20-02-2025, 17:53:33", + "recorded-content": { + "create-response-non-existent-subnet-id": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Error occurred while DescribeSubnets. EC2 Error Code: InvalidSubnetID.NotFound. EC2 Error Message: The subnet ID '' does not exist" + }, + "Type": "User", + "message": "Error occurred while DescribeSubnets. EC2 Error Code: InvalidSubnetID.NotFound. EC2 Error Message: The subnet ID '' does not exist", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "create-response-invalid-format-subnet-id": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value '[]' at 'vpcConfig.subnetIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 0, Member must satisfy regular expression pattern: ^subnet-[0-9a-z]*$]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_vpc_config_security_group": { + "recorded-date": "20-02-2025, 17:57:29", + "recorded-content": { + "create-response-non-existent-security-group": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Error occurred while DescribeSecurityGroups. EC2 Error Code: InvalidGroup.NotFound. EC2 Error Message: The security group '' does not exist" + }, + "Type": "User", + "message": "Error occurred while DescribeSecurityGroups. EC2 Error Code: InvalidGroup.NotFound. EC2 Error Message: The security group '' does not exist", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "create-response-invalid-format-security-group": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value '[]' at 'vpcConfig.securityGroupIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 0, Member must satisfy regular expression pattern: ^sg-[0-9a-zA-Z]*$]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_create_event_source_validation_kinesis": { + "recorded-date": "03-03-2025, 16:49:40", + "recorded-content": { + "no_starting_position": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null." + }, + "Type": "User", + "message": "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid_starting_position": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'invalid' at 'startingPosition' failed to satisfy constraint: Member must satisfy enum value set: [LATEST, AT_TIMESTAMP, TRIM_HORIZON]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[dotnet8]": { + "recorded-date": "01-04-2025, 13:31:15", "recorded-content": { "create_function_response": { "Architectures": [ @@ -18572,7 +18320,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "dotnet::Dotnet.Function::FunctionHandler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -18582,12 +18330,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs18.x", + "Runtime": "dotnet8", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -18620,7 +18368,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "dotnet::Dotnet.Function::FunctionHandler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18631,12 +18379,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs18.x", + "Runtime": "dotnet8", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Active", @@ -18668,7 +18416,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function::1", "FunctionName": "", - "Handler": "index.handler", + "Handler": "dotnet::Dotnet.Function::FunctionHandler", "LastModified": "date", "LastUpdateStatus": "Successful", "LoggingConfig": { @@ -18679,13 +18427,13 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs18.x", + "Runtime": "dotnet8", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" + "ApplyOn": "None", + "OptimizationStatus": "Off" }, "State": "Active", "Timeout": 5, @@ -18701,8 +18449,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs16.x]": { - "recorded-date": "09-12-2024, 14:49:37", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java11]": { + "recorded-date": "01-04-2025, 13:40:26", "recorded-content": { "create_function_response": { "Architectures": [ @@ -18716,7 +18464,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "index.handler", + "Handler": "echo.Handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -18726,12 +18474,12 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs16.x", + "Runtime": "java11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, "SnapStart": { - "ApplyOn": "PublishedVersions", + "ApplyOn": "None", "OptimizationStatus": "Off" }, "State": "Pending", @@ -18747,2951 +18495,15 @@ "HTTPStatusCode": 201 } }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs16.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs16.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.13]": { - "recorded-date": "09-12-2024, 14:51:06", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.13", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.13", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.13", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.12]": { - "recorded-date": "09-12-2024, 14:52:34", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.11]": { - "recorded-date": "09-12-2024, 14:54:02", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.10]": { - "recorded-date": "09-12-2024, 14:55:31", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.10", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.10", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.10", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.9]": { - "recorded-date": "09-12-2024, 14:56:58", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.9", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.9", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.9", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.8]": { - "recorded-date": "09-12-2024, 14:58:27", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java8.al2]": { - "recorded-date": "09-12-2024, 15:04:41", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java8.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java8.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java8.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[ruby3.2]": { - "recorded-date": "09-12-2024, 15:06:09", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[ruby3.3]": { - "recorded-date": "09-12-2024, 15:07:48", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.3", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.3", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.3", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[dotnet6]": { - "recorded-date": "09-12-2024, 15:10:39", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet6", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet6", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet6", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[dotnet8]": { - "recorded-date": "09-12-2024, 15:12:14", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[provided.al2023]": { - "recorded-date": "09-12-2024, 15:13:42", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2023", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2023", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2023", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[provided.al2]": { - "recorded-date": "09-12-2024, 15:15:16", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get_function_response_latest": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get_function_response_version_1": { - "Code": { - "Location": "", - "RepositoryType": "S3" - }, - "Configuration": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "version1", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function::1", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "Successful", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "On" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "1" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs22.x]": { - "recorded-date": "09-12-2024, 15:27:51", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs22.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs22.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs20.x]": { - "recorded-date": "09-12-2024, 15:27:54", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs20.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs20.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs18.x]": { - "recorded-date": "09-12-2024, 15:27:57", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs18.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs18.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs16.x]": { - "recorded-date": "09-12-2024, 15:27:59", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs16.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "index.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "nodejs16.x", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.13]": { - "recorded-date": "09-12-2024, 15:28:02", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.13", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.13", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.12]": { - "recorded-date": "09-12-2024, 15:28:04", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.12", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.11]": { - "recorded-date": "09-12-2024, 15:28:06", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.11", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.10]": { - "recorded-date": "09-12-2024, 15:28:09", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.10", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.10", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.9]": { - "recorded-date": "09-12-2024, 15:28:11", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.9", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.9", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.8]": { - "recorded-date": "09-12-2024, 15:28:13", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "handler.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "python3.8", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java8.al2]": { - "recorded-date": "09-12-2024, 15:28:24", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "echo.Handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "java8.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 + "update_function_response": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "EphemeralStorage": { + "Size": 512 }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", @@ -21708,99 +18520,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "java8.al2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "PublishedVersions", - "OptimizationStatus": "Off" - }, - "State": "Active", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[ruby3.2]": { - "recorded-date": "09-12-2024, 15:28:26", - "recorded-content": { - "create_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.2", - "RuntimeVersionConfig": { - "RuntimeVersionArn": "arn::lambda:::runtime:" - }, - "SnapStart": { - "ApplyOn": "None", - "OptimizationStatus": "Off" - }, - "State": "Pending", - "StateReason": "The function is being created.", - "StateReasonCode": "Creating", - "Timeout": 5, - "TracingConfig": { - "Mode": "PassThrough" - }, - "Version": "$LATEST", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "update_function_response": { - "Architectures": [ - "x86_64" - ], - "CodeSha256": "", - "CodeSize": "", - "Description": "", - "EphemeralStorage": { - "Size": 512 - }, - "FunctionArn": "arn::lambda::111111111111:function:", - "FunctionName": "", - "Handler": "function.handler", - "LastModified": "date", - "LastUpdateStatus": "InProgress", - "LastUpdateStatusReason": "The function is being created.", - "LastUpdateStatusReasonCode": "Creating", - "LoggingConfig": { - "LogFormat": "Text", - "LogGroup": "/aws/lambda/" - }, - "MemorySize": 1024, - "PackageType": "Zip", - "RevisionId": "", - "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.2", + "Runtime": "java11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -21821,8 +18541,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[ruby3.3]": { - "recorded-date": "09-12-2024, 15:28:29", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": { + "recorded-date": "01-04-2025, 13:40:32", "recorded-content": { "create_function_response": { "Architectures": [ @@ -21836,7 +18556,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "echo.Handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -21846,7 +18566,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.3", + "Runtime": "java17", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -21879,7 +18599,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "echo.Handler", "LastModified": "date", "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", @@ -21892,7 +18612,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "ruby3.3", + "Runtime": "java17", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -21913,8 +18633,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[dotnet6]": { - "recorded-date": "09-12-2024, 15:28:31", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": { + "recorded-date": "01-04-2025, 13:40:35", "recorded-content": { "create_function_response": { "Architectures": [ @@ -21928,7 +18648,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", + "Handler": "echo.Handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -21938,7 +18658,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet6", + "Runtime": "java21", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -21971,7 +18691,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", + "Handler": "echo.Handler", "LastModified": "date", "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", @@ -21984,7 +18704,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet6", + "Runtime": "java21", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22005,8 +18725,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[dotnet8]": { - "recorded-date": "09-12-2024, 15:28:33", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.12]": { + "recorded-date": "01-04-2025, 13:40:40", "recorded-content": { "create_function_response": { "Architectures": [ @@ -22020,7 +18740,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", + "Handler": "handler.handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -22030,7 +18750,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet8", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22063,7 +18783,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "dotnet::Dotnet.Function::FunctionHandler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", @@ -22076,7 +18796,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "dotnet8", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22097,8 +18817,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[provided.al2023]": { - "recorded-date": "09-12-2024, 15:28:36", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.13]": { + "recorded-date": "01-04-2025, 13:40:44", "recorded-content": { "create_function_response": { "Architectures": [ @@ -22112,7 +18832,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "handler.handler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -22122,7 +18842,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2023", + "Runtime": "python3.13", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22155,7 +18875,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", @@ -22168,7 +18888,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2023", + "Runtime": "python3.13", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22189,8 +18909,8 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[provided.al2]": { - "recorded-date": "09-12-2024, 15:28:38", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[dotnet8]": { + "recorded-date": "01-04-2025, 13:40:47", "recorded-content": { "create_function_response": { "Architectures": [ @@ -22204,7 +18924,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "dotnet::Dotnet.Function::FunctionHandler", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -22214,7 +18934,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2", + "Runtime": "dotnet8", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22247,7 +18967,7 @@ }, "FunctionArn": "arn::lambda::111111111111:function:", "FunctionName": "", - "Handler": "function.handler", + "Handler": "dotnet::Dotnet.Function::FunctionHandler", "LastModified": "date", "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", @@ -22260,7 +18980,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn::iam::111111111111:role/", - "Runtime": "provided.al2", + "Runtime": "dotnet8", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn::lambda:::runtime:" }, @@ -22280,86 +19000,5 @@ } } } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_vpc_config_subnet": { - "recorded-date": "20-02-2025, 17:53:33", - "recorded-content": { - "create-response-non-existent-subnet-id": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "Error occurred while DescribeSubnets. EC2 Error Code: InvalidSubnetID.NotFound. EC2 Error Message: The subnet ID '' does not exist" - }, - "Type": "User", - "message": "Error occurred while DescribeSubnets. EC2 Error Code: InvalidSubnetID.NotFound. EC2 Error Message: The subnet ID '' does not exist", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "create-response-invalid-format-subnet-id": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value '[]' at 'vpcConfig.subnetIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 0, Member must satisfy regular expression pattern: ^subnet-[0-9a-z]*$]" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_vpc_config_security_group": { - "recorded-date": "20-02-2025, 17:57:29", - "recorded-content": { - "create-response-non-existent-security-group": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "Error occurred while DescribeSecurityGroups. EC2 Error Code: InvalidGroup.NotFound. EC2 Error Message: The security group '' does not exist" - }, - "Type": "User", - "message": "Error occurred while DescribeSecurityGroups. EC2 Error Code: InvalidGroup.NotFound. EC2 Error Message: The security group '' does not exist", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "create-response-invalid-format-security-group": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value '[]' at 'vpcConfig.securityGroupIds' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 1024, Member must have length greater than or equal to 0, Member must satisfy regular expression pattern: ^sg-[0-9a-zA-Z]*$]" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_create_event_source_validation_kinesis": { - "recorded-date": "03-03-2025, 16:49:40", - "recorded-content": { - "no_starting_position": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null." - }, - "Type": "User", - "message": "1 validation error detected: Value null at 'startingPosition' failed to satisfy constraint: Member must not be null.", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "invalid_starting_position": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'invalid' at 'startingPosition' failed to satisfy constraint: Member must satisfy enum value set: [LATEST, AT_TIMESTAMP, TRIM_HORIZON]" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } } } diff --git a/tests/aws/services/lambda_/test_lambda_api.validation.json b/tests/aws/services/lambda_/test_lambda_api.validation.json index 80f0daa410637..757169d7ade65 100644 --- a/tests/aws/services/lambda_/test_lambda_api.validation.json +++ b/tests/aws/services/lambda_/test_lambda_api.validation.json @@ -63,7 +63,7 @@ "last_validated_date": "2024-04-10T08:58:47+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_create_lambda_exceptions": { - "last_validated_date": "2024-11-26T09:27:31+00:00" + "last_validated_date": "2025-04-01T13:08:49+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_delete_on_nonexisting_version": { "last_validated_date": "2024-09-12T11:29:32+00:00" @@ -426,7 +426,7 @@ "last_validated_date": "2024-09-12T11:29:23+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_update_lambda_exceptions": { - "last_validated_date": "2024-11-26T09:27:33+00:00" + "last_validated_date": "2025-04-01T13:10:29+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_vpc_config": { "last_validated_date": "2024-09-12T11:34:40+00:00" @@ -444,13 +444,13 @@ "last_validated_date": "2024-04-10T09:10:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "last_validated_date": "2024-11-26T09:27:34+00:00" + "last_validated_date": "2025-04-01T13:14:56+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "last_validated_date": "2024-11-26T09:27:38+00:00" + "last_validated_date": "2025-04-01T13:15:00+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": { - "last_validated_date": "2024-11-26T09:27:46+00:00" + "last_validated_date": "2025-04-01T13:19:40+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_exceptions": { "last_validated_date": "2024-04-10T09:23:18+00:00" @@ -546,127 +546,43 @@ "last_validated_date": "2024-04-10T09:17:26+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_exceptions": { - "last_validated_date": "2024-12-09T15:23:03+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[dotnet6]": { - "last_validated_date": "2024-12-09T15:10:39+00:00" + "last_validated_date": "2025-03-31T16:15:53+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[dotnet8]": { - "last_validated_date": "2024-12-09T15:12:13+00:00" + "last_validated_date": "2025-04-01T13:31:14+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java11]": { - "last_validated_date": "2024-12-09T15:03:11+00:00" + "last_validated_date": "2025-04-01T13:30:54+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java17]": { - "last_validated_date": "2024-12-09T15:01:47+00:00" + "last_validated_date": "2025-04-01T13:30:57+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java21]": { - "last_validated_date": "2024-12-09T15:00:19+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java8.al2]": { - "last_validated_date": "2024-12-09T15:04:40+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs16.x]": { - "last_validated_date": "2024-12-09T14:49:37+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs18.x]": { - "last_validated_date": "2024-12-09T14:48:09+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs20.x]": { - "last_validated_date": "2024-12-09T14:46:41+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[nodejs22.x]": { - "last_validated_date": "2024-12-09T14:45:02+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[provided.al2023]": { - "last_validated_date": "2024-12-09T15:13:42+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[provided.al2]": { - "last_validated_date": "2024-12-09T15:15:16+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.10]": { - "last_validated_date": "2024-12-09T14:55:30+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.11]": { - "last_validated_date": "2024-12-09T14:54:02+00:00" + "last_validated_date": "2025-04-01T13:31:02+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.12]": { - "last_validated_date": "2024-12-09T14:52:34+00:00" + "last_validated_date": "2025-04-01T13:31:06+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.13]": { - "last_validated_date": "2024-12-09T14:51:05+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.8]": { - "last_validated_date": "2024-12-09T14:58:27+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[python3.9]": { - "last_validated_date": "2024-12-09T14:56:58+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[ruby3.2]": { - "last_validated_date": "2024-12-09T15:06:09+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[ruby3.3]": { - "last_validated_date": "2024-12-09T15:07:48+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[dotnet6]": { - "last_validated_date": "2024-12-09T15:28:31+00:00" + "last_validated_date": "2025-04-01T13:31:10+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[dotnet8]": { - "last_validated_date": "2024-12-09T15:28:33+00:00" + "last_validated_date": "2025-04-01T13:42:13+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java11]": { - "last_validated_date": "2024-12-09T15:28:20+00:00" + "last_validated_date": "2025-04-01T13:41:52+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": { - "last_validated_date": "2024-12-09T15:28:18+00:00" + "last_validated_date": "2025-04-01T13:41:56+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": { - "last_validated_date": "2024-12-09T15:28:15+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java8.al2]": { - "last_validated_date": "2024-12-09T15:28:24+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs16.x]": { - "last_validated_date": "2024-12-09T15:27:59+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs18.x]": { - "last_validated_date": "2024-12-09T15:27:57+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs20.x]": { - "last_validated_date": "2024-12-09T15:27:53+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[nodejs22.x]": { - "last_validated_date": "2024-12-09T15:27:51+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[provided.al2023]": { - "last_validated_date": "2024-12-09T15:28:36+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[provided.al2]": { - "last_validated_date": "2024-12-09T15:28:38+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.10]": { - "last_validated_date": "2024-12-09T15:28:09+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.11]": { - "last_validated_date": "2024-12-09T15:28:06+00:00" + "last_validated_date": "2025-04-01T13:42:01+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.12]": { - "last_validated_date": "2024-12-09T15:28:03+00:00" + "last_validated_date": "2025-04-01T13:42:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.13]": { - "last_validated_date": "2024-12-09T15:28:01+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.8]": { - "last_validated_date": "2024-12-09T15:28:13+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[python3.9]": { - "last_validated_date": "2024-12-09T15:28:11+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[ruby3.2]": { - "last_validated_date": "2024-12-09T15:28:26+00:00" - }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[ruby3.3]": { - "last_validated_date": "2024-12-09T15:28:29+00:00" + "last_validated_date": "2025-04-01T13:42:08+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_create_tag_on_esm_create": { "last_validated_date": "2024-10-24T14:16:05+00:00" @@ -752,31 +668,28 @@ "tests/aws/services/lambda_/test_lambda_api.py::TestPartialARNMatching::test_update_function_configuration_full_arn": { "last_validated_date": "2024-06-05T11:49:05+00:00" }, - "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled": { - "last_validated_date": "2024-06-12T14:19:11+00:00" - }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[dotnetcore3.1]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[go1.x]": { - "last_validated_date": "2024-11-26T09:27:29+00:00" + "last_validated_date": "2025-04-01T13:06:03+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[java8]": { - "last_validated_date": "2024-11-26T09:27:29+00:00" + "last_validated_date": "2025-04-01T13:06:03+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[nodejs12.x]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:05+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[nodejs14.x]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[provided]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[python3.7]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestRuntimeValidation::test_create_deprecated_function_runtime_with_validation_enabled[ruby2.7]": { - "last_validated_date": "2024-11-26T09:27:30+00:00" + "last_validated_date": "2025-04-01T13:06:04+00:00" } } diff --git a/tests/aws/services/lambda_/test_lambda_common.snapshot.json b/tests/aws/services/lambda_/test_lambda_common.snapshot.json index cc5b9054d5f5e..262931448bb8c 100644 --- a/tests/aws/services/lambda_/test_lambda_common.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_common.snapshot.json @@ -1,70 +1,70 @@ { "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.8]": { - "recorded-date": "26-11-2024, 09:28:27", + "recorded-date": "31-03-2025, 12:14:56", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java11]": { - "recorded-date": "26-11-2024, 09:29:13", + "recorded-date": "31-03-2025, 12:15:23", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2]": { - "recorded-date": "26-11-2024, 09:31:05", + "recorded-date": "31-03-2025, 12:17:30", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java8.al2]": { - "recorded-date": "26-11-2024, 09:29:18", + "recorded-date": "31-03-2025, 12:15:42", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.2]": { - "recorded-date": "26-11-2024, 09:29:22", + "recorded-date": "31-03-2025, 12:15:54", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.11]": { - "recorded-date": "26-11-2024, 09:28:14", + "recorded-date": "31-03-2025, 12:14:05", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java17]": { - "recorded-date": "26-11-2024, 09:29:09", + "recorded-date": "31-03-2025, 12:15:14", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs18.x]": { - "recorded-date": "26-11-2024, 09:27:58", + "recorded-date": "31-03-2025, 12:13:11", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java21]": { - "recorded-date": "26-11-2024, 09:29:04", + "recorded-date": "31-03-2025, 12:15:05", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2023]": { - "recorded-date": "26-11-2024, 09:30:57", + "recorded-date": "31-03-2025, 12:17:17", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.9]": { - "recorded-date": "26-11-2024, 09:28:22", + "recorded-date": "31-03-2025, 12:14:39", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.10]": { - "recorded-date": "26-11-2024, 09:28:18", + "recorded-date": "31-03-2025, 12:14:20", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.12]": { - "recorded-date": "26-11-2024, 09:28:11", + "recorded-date": "31-03-2025, 12:13:57", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs20.x]": { - "recorded-date": "26-11-2024, 09:27:54", + "recorded-date": "31-03-2025, 12:12:59", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet6]": { - "recorded-date": "26-11-2024, 09:30:41", + "recorded-date": "31-03-2025, 12:16:46", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs16.x]": { - "recorded-date": "26-11-2024, 09:28:02", + "recorded-date": "31-03-2025, 12:13:31", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.8]": { - "recorded-date": "26-11-2024, 09:31:29", + "recorded-date": "31-03-2025, 12:18:00", "recorded-content": { "create_function_result": { "Architectures": [ @@ -205,7 +205,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java11]": { - "recorded-date": "26-11-2024, 09:31:44", + "recorded-date": "31-03-2025, 12:18:12", "recorded-content": { "create_function_result": { "Architectures": [ @@ -344,7 +344,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2]": { - "recorded-date": "26-11-2024, 09:33:29", + "recorded-date": "31-03-2025, 12:21:46", "recorded-content": { "create_function_result": { "Architectures": [ @@ -477,7 +477,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java8.al2]": { - "recorded-date": "26-11-2024, 09:31:47", + "recorded-date": "31-03-2025, 12:18:15", "recorded-content": { "create_function_result": { "Architectures": [ @@ -616,7 +616,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.2]": { - "recorded-date": "26-11-2024, 09:31:49", + "recorded-date": "31-03-2025, 12:18:19", "recorded-content": { "create_function_result": { "Architectures": [ @@ -761,7 +761,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.11]": { - "recorded-date": "26-11-2024, 09:31:22", + "recorded-date": "31-03-2025, 12:17:51", "recorded-content": { "create_function_result": { "Architectures": [ @@ -902,7 +902,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java17]": { - "recorded-date": "26-11-2024, 09:31:41", + "recorded-date": "31-03-2025, 12:18:08", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1037,7 +1037,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs18.x]": { - "recorded-date": "26-11-2024, 09:31:13", + "recorded-date": "31-03-2025, 12:17:39", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1178,7 +1178,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java21]": { - "recorded-date": "26-11-2024, 09:31:38", + "recorded-date": "31-03-2025, 12:18:04", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1313,7 +1313,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2023]": { - "recorded-date": "26-11-2024, 09:33:25", + "recorded-date": "31-03-2025, 12:21:36", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1446,7 +1446,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.9]": { - "recorded-date": "26-11-2024, 09:31:26", + "recorded-date": "31-03-2025, 12:17:57", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1587,7 +1587,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.10]": { - "recorded-date": "26-11-2024, 09:31:24", + "recorded-date": "31-03-2025, 12:17:54", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1728,7 +1728,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.12]": { - "recorded-date": "26-11-2024, 09:31:20", + "recorded-date": "31-03-2025, 12:17:48", "recorded-content": { "create_function_result": { "Architectures": [ @@ -1871,7 +1871,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs20.x]": { - "recorded-date": "26-11-2024, 09:31:10", + "recorded-date": "31-03-2025, 12:17:36", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2010,7 +2010,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet6]": { - "recorded-date": "26-11-2024, 09:32:01", + "recorded-date": "31-03-2025, 12:18:32", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2155,7 +2155,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs16.x]": { - "recorded-date": "26-11-2024, 09:31:15", + "recorded-date": "31-03-2025, 12:17:42", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2296,7 +2296,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.8]": { - "recorded-date": "26-11-2024, 09:33:50", + "recorded-date": "31-03-2025, 12:22:12", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2358,7 +2358,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java11]": { - "recorded-date": "26-11-2024, 09:34:06", + "recorded-date": "31-03-2025, 12:22:27", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2420,7 +2420,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2]": { - "recorded-date": "26-11-2024, 09:35:05", + "recorded-date": "31-03-2025, 12:26:16", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2481,7 +2481,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java8.al2]": { - "recorded-date": "26-11-2024, 09:34:09", + "recorded-date": "31-03-2025, 12:22:31", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2543,7 +2543,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.2]": { - "recorded-date": "26-11-2024, 09:34:11", + "recorded-date": "31-03-2025, 12:22:34", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2605,7 +2605,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.11]": { - "recorded-date": "26-11-2024, 09:33:44", + "recorded-date": "31-03-2025, 12:22:04", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2668,7 +2668,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java17]": { - "recorded-date": "26-11-2024, 09:34:03", + "recorded-date": "31-03-2025, 12:22:24", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2730,7 +2730,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs18.x]": { - "recorded-date": "26-11-2024, 09:33:36", + "recorded-date": "31-03-2025, 12:21:54", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2792,7 +2792,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java21]": { - "recorded-date": "26-11-2024, 09:34:00", + "recorded-date": "31-03-2025, 12:22:21", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2854,7 +2854,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2023]": { - "recorded-date": "26-11-2024, 09:35:00", + "recorded-date": "31-03-2025, 12:26:03", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2915,7 +2915,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.9]": { - "recorded-date": "26-11-2024, 09:33:48", + "recorded-date": "31-03-2025, 12:22:09", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2978,7 +2978,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.10]": { - "recorded-date": "26-11-2024, 09:33:46", + "recorded-date": "31-03-2025, 12:22:07", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3041,7 +3041,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.12]": { - "recorded-date": "26-11-2024, 09:33:42", + "recorded-date": "31-03-2025, 12:22:02", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3104,7 +3104,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs20.x]": { - "recorded-date": "26-11-2024, 09:33:34", + "recorded-date": "31-03-2025, 12:21:51", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3166,7 +3166,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet6]": { - "recorded-date": "26-11-2024, 09:34:22", + "recorded-date": "31-03-2025, 12:22:49", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3228,7 +3228,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs16.x]": { - "recorded-date": "26-11-2024, 09:33:38", + "recorded-date": "31-03-2025, 12:21:57", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3290,7 +3290,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.8]": { - "recorded-date": "26-11-2024, 09:35:17", + "recorded-date": "31-03-2025, 12:26:39", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3343,7 +3343,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java11]": { - "recorded-date": "26-11-2024, 09:35:23", + "recorded-date": "31-03-2025, 12:26:57", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3396,7 +3396,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java8.al2]": { - "recorded-date": "26-11-2024, 09:35:33", + "recorded-date": "31-03-2025, 12:27:11", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3449,7 +3449,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.2]": { - "recorded-date": "26-11-2024, 09:35:20", + "recorded-date": "31-03-2025, 12:26:45", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3502,7 +3502,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.11]": { - "recorded-date": "26-11-2024, 09:35:49", + "recorded-date": "31-03-2025, 12:26:19", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3555,7 +3555,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java17]": { - "recorded-date": "26-11-2024, 09:35:45", + "recorded-date": "31-03-2025, 12:26:29", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3608,7 +3608,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs18.x]": { - "recorded-date": "26-11-2024, 09:35:35", + "recorded-date": "31-03-2025, 12:26:41", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3661,7 +3661,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java21]": { - "recorded-date": "26-11-2024, 09:35:30", + "recorded-date": "31-03-2025, 12:27:07", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3714,7 +3714,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.9]": { - "recorded-date": "26-11-2024, 09:35:37", + "recorded-date": "31-03-2025, 12:27:16", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3767,7 +3767,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.10]": { - "recorded-date": "26-11-2024, 09:35:11", + "recorded-date": "31-03-2025, 12:27:14", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3820,7 +3820,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.12]": { - "recorded-date": "26-11-2024, 09:35:25", + "recorded-date": "31-03-2025, 12:27:00", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3873,7 +3873,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs20.x]": { - "recorded-date": "26-11-2024, 09:35:47", + "recorded-date": "31-03-2025, 12:26:50", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3926,7 +3926,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet6]": { - "recorded-date": "26-11-2024, 09:35:40", + "recorded-date": "31-03-2025, 12:26:33", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3979,7 +3979,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs16.x]": { - "recorded-date": "26-11-2024, 09:35:27", + "recorded-date": "31-03-2025, 12:26:25", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4032,47 +4032,47 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs20.x]": { - "recorded-date": "26-11-2024, 09:38:09", + "recorded-date": "31-03-2025, 17:43:04", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs18.x]": { - "recorded-date": "26-11-2024, 09:37:43", + "recorded-date": "31-03-2025, 17:42:22", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs16.x]": { - "recorded-date": "26-11-2024, 09:36:44", + "recorded-date": "31-03-2025, 17:44:41", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.8]": { - "recorded-date": "26-11-2024, 09:36:15", + "recorded-date": "31-03-2025, 17:43:58", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.9]": { - "recorded-date": "26-11-2024, 09:37:45", + "recorded-date": "31-03-2025, 17:44:04", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.10]": { - "recorded-date": "26-11-2024, 09:35:56", + "recorded-date": "31-03-2025, 17:42:25", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.11]": { - "recorded-date": "26-11-2024, 09:38:12", + "recorded-date": "31-03-2025, 17:43:22", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.12]": { - "recorded-date": "26-11-2024, 09:36:40", + "recorded-date": "31-03-2025, 17:44:01", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.2]": { - "recorded-date": "26-11-2024, 09:36:18", + "recorded-date": "31-03-2025, 17:43:19", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet8]": { - "recorded-date": "26-11-2024, 09:30:52", + "recorded-date": "31-03-2025, 12:17:03", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet8]": { - "recorded-date": "26-11-2024, 09:32:13", + "recorded-date": "31-03-2025, 12:18:36", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4219,7 +4219,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet8]": { - "recorded-date": "26-11-2024, 09:34:30", + "recorded-date": "31-03-2025, 12:22:56", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4281,7 +4281,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet8]": { - "recorded-date": "26-11-2024, 09:35:15", + "recorded-date": "31-03-2025, 12:27:03", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4334,35 +4334,35 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java17]": { - "recorded-date": "26-11-2024, 09:38:07", + "recorded-date": "31-03-2025, 17:42:41", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java21]": { - "recorded-date": "26-11-2024, 09:36:54", + "recorded-date": "31-03-2025, 17:43:01", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java11]": { - "recorded-date": "26-11-2024, 09:36:37", + "recorded-date": "31-03-2025, 17:44:31", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java8.al2]": { - "recorded-date": "26-11-2024, 09:37:40", + "recorded-date": "31-03-2025, 17:43:50", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet6]": { - "recorded-date": "26-11-2024, 09:37:55", + "recorded-date": "31-03-2025, 17:43:15", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet8]": { - "recorded-date": "26-11-2024, 09:36:12", + "recorded-date": "31-03-2025, 17:43:54", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.3]": { - "recorded-date": "26-11-2024, 09:29:26", + "recorded-date": "31-03-2025, 12:16:02", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.3]": { - "recorded-date": "26-11-2024, 09:31:52", + "recorded-date": "31-03-2025, 12:18:22", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4507,7 +4507,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.3]": { - "recorded-date": "26-11-2024, 09:34:14", + "recorded-date": "31-03-2025, 12:22:37", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4569,7 +4569,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.3]": { - "recorded-date": "26-11-2024, 09:35:08", + "recorded-date": "31-03-2025, 12:26:22", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4622,15 +4622,15 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.3]": { - "recorded-date": "26-11-2024, 09:35:53", + "recorded-date": "31-03-2025, 17:44:35", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.13]": { - "recorded-date": "26-11-2024, 09:28:06", + "recorded-date": "31-03-2025, 12:13:46", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.13]": { - "recorded-date": "26-11-2024, 09:31:18", + "recorded-date": "31-03-2025, 12:17:45", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4773,7 +4773,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.13]": { - "recorded-date": "26-11-2024, 09:33:40", + "recorded-date": "31-03-2025, 12:21:59", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4836,7 +4836,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.13]": { - "recorded-date": "26-11-2024, 09:35:13", + "recorded-date": "31-03-2025, 12:26:53", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4889,15 +4889,15 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.13]": { - "recorded-date": "26-11-2024, 09:35:58", + "recorded-date": "31-03-2025, 17:44:38", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs22.x]": { - "recorded-date": "26-11-2024, 09:27:50", + "recorded-date": "31-03-2025, 12:12:50", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs22.x]": { - "recorded-date": "26-11-2024, 09:31:08", + "recorded-date": "31-03-2025, 12:17:33", "recorded-content": { "create_function_result": { "Architectures": [ @@ -5036,7 +5036,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs22.x]": { - "recorded-date": "26-11-2024, 09:33:31", + "recorded-date": "31-03-2025, 12:21:49", "recorded-content": { "create_function_result": { "Architectures": [ @@ -5098,7 +5098,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs22.x]": { - "recorded-date": "26-11-2024, 09:35:42", + "recorded-date": "31-03-2025, 12:26:36", "recorded-content": { "create_function_result": { "Architectures": [ @@ -5151,7 +5151,275 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs22.x]": { - "recorded-date": "26-11-2024, 09:37:57", + "recorded-date": "31-03-2025, 17:42:44", + "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.4]": { + "recorded-date": "31-03-2025, 12:16:24", + "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.4]": { + "recorded-date": "31-03-2025, 12:18:27", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": { + "TEST_KEY": "TEST_VAL" + } + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn::iam::111111111111:role/", + "Runtime": "ruby3.4", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn::lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "invocation_result_payload": { + "ctx": { + "aws_request_id": "", + "function_name": "", + "function_version": "$LATEST", + "invoked_function_arn": "arn::lambda::111111111111:function:", + "log_group_name": "/aws/lambda/", + "log_stream_name": "", + "memory_limit_in_mb": "1024", + "remaining_time_in_millis": "" + }, + "environment": { + "AWS_ACCESS_KEY_ID": "", + "AWS_DEFAULT_REGION": "", + "AWS_EXECUTION_ENV": "AWS_Lambda_ruby3.4", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_NAME": "", + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand", + "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/", + "AWS_LAMBDA_LOG_STREAM_NAME": "", + "AWS_LAMBDA_RUNTIME_API": "169.254.100.1:9001", + "AWS_REGION": "", + "AWS_SECRET_ACCESS_KEY": "", + "AWS_SESSION_TOKEN": "", + "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR", + "AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1:2000", + "GEM_HOME": "/var/runtime", + "GEM_PATH": "/var/task/vendor/bundle/ruby/3.4.0:/opt/ruby/gems/3.4.0:/var/runtime:/var/runtime/ruby/3.4.0", + "LAMBDA_RUNTIME_DIR": "/var/runtime", + "LAMBDA_TASK_ROOT": "/var/task", + "LANG": "en_US.UTF-8", + "LD_LIBRARY_PATH": "/var/lang/lib:/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", + "PATH": "/var/lang/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", + "PWD": "/var/task", + "RUBYLIB": "/var/runtime/gems/aws_lambda_ric-3.0.0/lib:/var/task:/var/runtime/lib:/opt/ruby/lib", + "SHLVL": "0", + "TEST_KEY": "TEST_VAL", + "TZ": ":UTC", + "_AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1", + "_AWS_XRAY_DAEMON_PORT": "2000", + "_HANDLER": "function.handler", + "_X_AMZN_TRACE_ID": "" + }, + "packages": [] + }, + "invocation_result_payload_qualified": { + "ctx": { + "aws_request_id": "", + "function_name": "", + "function_version": "$LATEST", + "invoked_function_arn": "arn::lambda::111111111111:function::$LATEST", + "log_group_name": "/aws/lambda/", + "log_stream_name": "", + "memory_limit_in_mb": "1024", + "remaining_time_in_millis": "" + }, + "environment": { + "AWS_ACCESS_KEY_ID": "", + "AWS_DEFAULT_REGION": "", + "AWS_EXECUTION_ENV": "AWS_Lambda_ruby3.4", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_NAME": "", + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand", + "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/", + "AWS_LAMBDA_LOG_STREAM_NAME": "", + "AWS_LAMBDA_RUNTIME_API": "169.254.100.1:9001", + "AWS_REGION": "", + "AWS_SECRET_ACCESS_KEY": "", + "AWS_SESSION_TOKEN": "", + "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR", + "AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1:2000", + "GEM_HOME": "/var/runtime", + "GEM_PATH": "/var/task/vendor/bundle/ruby/3.4.0:/opt/ruby/gems/3.4.0:/var/runtime:/var/runtime/ruby/3.4.0", + "LAMBDA_RUNTIME_DIR": "/var/runtime", + "LAMBDA_TASK_ROOT": "/var/task", + "LANG": "en_US.UTF-8", + "LD_LIBRARY_PATH": "/var/lang/lib:/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", + "PATH": "/var/lang/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", + "PWD": "/var/task", + "RUBYLIB": "/var/runtime/gems/aws_lambda_ric-3.0.0/lib:/var/task:/var/runtime/lib:/opt/ruby/lib", + "SHLVL": "0", + "TEST_KEY": "TEST_VAL", + "TZ": ":UTC", + "_AWS_XRAY_DAEMON_ADDRESS": "169.254.100.1", + "_AWS_XRAY_DAEMON_PORT": "2000", + "_HANDLER": "function.handler", + "_X_AMZN_TRACE_ID": "" + }, + "packages": [] + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.4]": { + "recorded-date": "31-03-2025, 12:22:40", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn::iam::111111111111:role/", + "Runtime": "ruby3.4", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn::lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "error_result": { + "ExecutedVersion": "$LATEST", + "FunctionError": "Unhandled", + "Payload": { + "errorMessage": "Error: some_error_msg", + "errorType": "Function", + "stackTrace": "" + }, + "StatusCode": 200, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.4]": { + "recorded-date": "31-03-2025, 12:26:47", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": { + "AWS_LAMBDA_EXEC_WRAPPER": "/var/task/environment_wrapper" + } + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn::iam::111111111111:role/", + "Runtime": "ruby3.4", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn::lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.4]": { + "recorded-date": "31-03-2025, 17:43:08", "recorded-content": {} } } diff --git a/tests/aws/services/lambda_/test_lambda_common.validation.json b/tests/aws/services/lambda_/test_lambda_common.validation.json index 5eb2b062f413c..9ea9db3a25ba3 100644 --- a/tests/aws/services/lambda_/test_lambda_common.validation.json +++ b/tests/aws/services/lambda_/test_lambda_common.validation.json @@ -1,290 +1,305 @@ { "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet6]": { - "last_validated_date": "2024-11-26T09:37:54+00:00" + "last_validated_date": "2025-03-31T17:43:14+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet8]": { - "last_validated_date": "2024-11-26T09:36:12+00:00" + "last_validated_date": "2025-03-31T17:43:54+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java11]": { - "last_validated_date": "2024-11-26T09:36:37+00:00" + "last_validated_date": "2025-03-31T17:44:30+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java17]": { - "last_validated_date": "2024-11-26T09:38:07+00:00" + "last_validated_date": "2025-03-31T17:42:41+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java21]": { - "last_validated_date": "2024-11-26T09:36:53+00:00" + "last_validated_date": "2025-03-31T17:43:00+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java8.al2]": { - "last_validated_date": "2024-11-26T09:37:40+00:00" + "last_validated_date": "2025-03-31T17:43:49+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs16.x]": { - "last_validated_date": "2024-11-26T09:36:43+00:00" + "last_validated_date": "2025-03-31T17:44:41+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs18.x]": { - "last_validated_date": "2024-11-26T09:37:42+00:00" + "last_validated_date": "2025-03-31T17:42:21+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs20.x]": { - "last_validated_date": "2024-11-26T09:38:09+00:00" + "last_validated_date": "2025-03-31T17:43:04+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs22.x]": { - "last_validated_date": "2024-11-26T09:37:57+00:00" + "last_validated_date": "2025-03-31T17:42:44+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.10]": { - "last_validated_date": "2024-11-26T09:35:55+00:00" + "last_validated_date": "2025-03-31T17:42:24+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.11]": { - "last_validated_date": "2024-11-26T09:38:12+00:00" + "last_validated_date": "2025-03-31T17:43:21+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.12]": { - "last_validated_date": "2024-11-26T09:36:40+00:00" + "last_validated_date": "2025-03-31T17:44:00+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.13]": { - "last_validated_date": "2024-11-26T09:35:58+00:00" + "last_validated_date": "2025-03-31T17:44:37+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.8]": { - "last_validated_date": "2024-11-26T09:36:14+00:00" + "last_validated_date": "2025-03-31T17:43:57+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.9]": { - "last_validated_date": "2024-11-26T09:37:45+00:00" + "last_validated_date": "2025-03-31T17:44:04+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.2]": { - "last_validated_date": "2024-11-26T09:36:18+00:00" + "last_validated_date": "2025-03-31T17:43:18+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.3]": { - "last_validated_date": "2024-11-26T09:35:53+00:00" + "last_validated_date": "2025-03-31T17:44:34+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.4]": { + "last_validated_date": "2025-03-31T17:43:07+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet6]": { - "last_validated_date": "2024-11-26T09:30:41+00:00" + "last_validated_date": "2025-03-31T12:16:46+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet8]": { - "last_validated_date": "2024-11-26T09:30:52+00:00" + "last_validated_date": "2025-03-31T12:17:03+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java11]": { - "last_validated_date": "2024-11-26T09:29:13+00:00" + "last_validated_date": "2025-03-31T12:15:22+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java17]": { - "last_validated_date": "2024-11-26T09:29:09+00:00" + "last_validated_date": "2025-03-31T12:15:13+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java21]": { - "last_validated_date": "2024-11-26T09:29:04+00:00" + "last_validated_date": "2025-03-31T12:15:05+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java8.al2]": { - "last_validated_date": "2024-11-26T09:29:18+00:00" + "last_validated_date": "2025-03-31T12:15:42+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs16.x]": { - "last_validated_date": "2024-11-26T09:28:02+00:00" + "last_validated_date": "2025-03-31T12:13:31+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs18.x]": { - "last_validated_date": "2024-11-26T09:27:58+00:00" + "last_validated_date": "2025-03-31T12:13:11+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs20.x]": { - "last_validated_date": "2024-11-26T09:27:54+00:00" + "last_validated_date": "2025-03-31T12:12:59+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs22.x]": { - "last_validated_date": "2024-11-26T09:27:50+00:00" + "last_validated_date": "2025-03-31T12:12:50+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2023]": { - "last_validated_date": "2024-11-26T09:30:57+00:00" + "last_validated_date": "2025-03-31T12:17:17+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2]": { - "last_validated_date": "2024-11-26T09:31:05+00:00" + "last_validated_date": "2025-03-31T12:17:30+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.10]": { - "last_validated_date": "2024-11-26T09:28:18+00:00" + "last_validated_date": "2025-03-31T12:14:20+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.11]": { - "last_validated_date": "2024-11-26T09:28:14+00:00" + "last_validated_date": "2025-03-31T12:14:05+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.12]": { - "last_validated_date": "2024-11-26T09:28:10+00:00" + "last_validated_date": "2025-03-31T12:13:57+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.13]": { - "last_validated_date": "2024-11-26T09:28:06+00:00" + "last_validated_date": "2025-03-31T12:13:45+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.8]": { - "last_validated_date": "2024-11-26T09:28:27+00:00" + "last_validated_date": "2025-03-31T12:14:56+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.9]": { - "last_validated_date": "2024-11-26T09:28:22+00:00" + "last_validated_date": "2025-03-31T12:14:39+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.2]": { - "last_validated_date": "2024-11-26T09:29:22+00:00" + "last_validated_date": "2025-03-31T12:15:53+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.3]": { - "last_validated_date": "2024-11-26T09:29:26+00:00" + "last_validated_date": "2025-03-31T12:16:02+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.4]": { + "last_validated_date": "2025-03-31T12:16:24+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet6]": { - "last_validated_date": "2024-11-26T09:32:01+00:00" + "last_validated_date": "2025-03-31T12:18:32+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet8]": { - "last_validated_date": "2024-11-26T09:32:13+00:00" + "last_validated_date": "2025-03-31T12:18:35+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java11]": { - "last_validated_date": "2024-11-26T09:31:43+00:00" + "last_validated_date": "2025-03-31T12:18:11+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java17]": { - "last_validated_date": "2024-11-26T09:31:40+00:00" + "last_validated_date": "2025-03-31T12:18:07+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java21]": { - "last_validated_date": "2024-11-26T09:31:38+00:00" + "last_validated_date": "2025-03-31T12:18:04+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java8.al2]": { - "last_validated_date": "2024-11-26T09:31:46+00:00" + "last_validated_date": "2025-03-31T12:18:15+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs16.x]": { - "last_validated_date": "2024-11-26T09:31:15+00:00" + "last_validated_date": "2025-03-31T12:17:42+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs18.x]": { - "last_validated_date": "2024-11-26T09:31:12+00:00" + "last_validated_date": "2025-03-31T12:17:39+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs20.x]": { - "last_validated_date": "2024-11-26T09:31:10+00:00" + "last_validated_date": "2025-03-31T12:17:36+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs22.x]": { - "last_validated_date": "2024-11-26T09:31:08+00:00" + "last_validated_date": "2025-03-31T12:17:33+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2023]": { - "last_validated_date": "2024-11-26T09:33:24+00:00" + "last_validated_date": "2025-03-31T12:21:35+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2]": { - "last_validated_date": "2024-11-26T09:33:29+00:00" + "last_validated_date": "2025-03-31T12:21:46+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.10]": { - "last_validated_date": "2024-11-26T09:31:24+00:00" + "last_validated_date": "2025-03-31T12:17:54+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.11]": { - "last_validated_date": "2024-11-26T09:31:22+00:00" + "last_validated_date": "2025-03-31T12:17:51+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.12]": { - "last_validated_date": "2024-11-26T09:31:20+00:00" + "last_validated_date": "2025-03-31T12:17:48+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.13]": { - "last_validated_date": "2024-11-26T09:31:17+00:00" + "last_validated_date": "2025-03-31T12:17:45+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.8]": { - "last_validated_date": "2024-11-26T09:31:28+00:00" + "last_validated_date": "2025-03-31T12:18:00+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.9]": { - "last_validated_date": "2024-11-26T09:31:26+00:00" + "last_validated_date": "2025-03-31T12:17:56+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.2]": { - "last_validated_date": "2024-11-26T09:31:49+00:00" + "last_validated_date": "2025-03-31T12:18:18+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.3]": { - "last_validated_date": "2024-11-26T09:31:51+00:00" + "last_validated_date": "2025-03-31T12:18:21+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.4]": { + "last_validated_date": "2025-03-31T12:18:26+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet6]": { - "last_validated_date": "2024-11-26T09:35:40+00:00" + "last_validated_date": "2025-03-31T12:26:32+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet8]": { - "last_validated_date": "2024-11-26T09:35:15+00:00" + "last_validated_date": "2025-03-31T12:27:03+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java11]": { - "last_validated_date": "2024-11-26T09:35:22+00:00" + "last_validated_date": "2025-03-31T12:26:57+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java17]": { - "last_validated_date": "2024-11-26T09:35:45+00:00" + "last_validated_date": "2025-03-31T12:26:29+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java21]": { - "last_validated_date": "2024-11-26T09:35:29+00:00" + "last_validated_date": "2025-03-31T12:27:06+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java8.al2]": { - "last_validated_date": "2024-11-26T09:35:33+00:00" + "last_validated_date": "2025-03-31T12:27:10+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs16.x]": { - "last_validated_date": "2024-11-26T09:35:27+00:00" + "last_validated_date": "2025-03-31T12:26:24+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs18.x]": { - "last_validated_date": "2024-11-26T09:35:35+00:00" + "last_validated_date": "2025-03-31T12:26:41+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs20.x]": { - "last_validated_date": "2024-11-26T09:35:47+00:00" + "last_validated_date": "2025-03-31T12:26:50+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs22.x]": { - "last_validated_date": "2024-11-26T09:35:42+00:00" + "last_validated_date": "2025-03-31T12:26:35+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.10]": { - "last_validated_date": "2024-11-26T09:35:10+00:00" + "last_validated_date": "2025-03-31T12:27:13+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.11]": { - "last_validated_date": "2024-11-26T09:35:49+00:00" + "last_validated_date": "2025-03-31T12:26:18+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.12]": { - "last_validated_date": "2024-11-26T09:35:24+00:00" + "last_validated_date": "2025-03-31T12:27:00+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.13]": { - "last_validated_date": "2024-11-26T09:35:12+00:00" + "last_validated_date": "2025-03-31T12:26:53+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.8]": { - "last_validated_date": "2024-11-26T09:35:17+00:00" + "last_validated_date": "2025-03-31T12:26:38+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.9]": { - "last_validated_date": "2024-11-26T09:35:37+00:00" + "last_validated_date": "2025-03-31T12:27:16+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.2]": { - "last_validated_date": "2024-11-26T09:35:20+00:00" + "last_validated_date": "2025-03-31T12:26:44+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.3]": { - "last_validated_date": "2024-11-26T09:35:08+00:00" + "last_validated_date": "2025-03-31T12:26:21+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.4]": { + "last_validated_date": "2025-03-31T12:26:47+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet6]": { - "last_validated_date": "2024-11-26T09:34:21+00:00" + "last_validated_date": "2025-03-31T12:22:48+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet8]": { - "last_validated_date": "2024-11-26T09:34:30+00:00" + "last_validated_date": "2025-03-31T12:22:56+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java11]": { - "last_validated_date": "2024-11-26T09:34:06+00:00" + "last_validated_date": "2025-03-31T12:22:27+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java17]": { - "last_validated_date": "2024-11-26T09:34:03+00:00" + "last_validated_date": "2025-03-31T12:22:23+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java21]": { - "last_validated_date": "2024-11-26T09:34:00+00:00" + "last_validated_date": "2025-03-31T12:22:21+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java8.al2]": { - "last_validated_date": "2024-11-26T09:34:09+00:00" + "last_validated_date": "2025-03-31T12:22:31+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs16.x]": { - "last_validated_date": "2024-11-26T09:33:37+00:00" + "last_validated_date": "2025-03-31T12:21:56+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs18.x]": { - "last_validated_date": "2024-11-26T09:33:35+00:00" + "last_validated_date": "2025-03-31T12:21:54+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs20.x]": { - "last_validated_date": "2024-11-26T09:33:33+00:00" + "last_validated_date": "2025-03-31T12:21:51+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs22.x]": { - "last_validated_date": "2024-11-26T09:33:31+00:00" + "last_validated_date": "2025-03-31T12:21:48+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2023]": { - "last_validated_date": "2024-11-26T09:35:00+00:00" + "last_validated_date": "2025-03-31T12:26:02+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2]": { - "last_validated_date": "2024-11-26T09:35:05+00:00" + "last_validated_date": "2025-03-31T12:26:16+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.10]": { - "last_validated_date": "2024-11-26T09:33:46+00:00" + "last_validated_date": "2025-03-31T12:22:07+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.11]": { - "last_validated_date": "2024-11-26T09:33:44+00:00" + "last_validated_date": "2025-03-31T12:22:04+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.12]": { - "last_validated_date": "2024-11-26T09:33:42+00:00" + "last_validated_date": "2025-03-31T12:22:02+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.13]": { - "last_validated_date": "2024-11-26T09:33:40+00:00" + "last_validated_date": "2025-03-31T12:21:59+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.8]": { - "last_validated_date": "2024-11-26T09:33:50+00:00" + "last_validated_date": "2025-03-31T12:22:12+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.9]": { - "last_validated_date": "2024-11-26T09:33:48+00:00" + "last_validated_date": "2025-03-31T12:22:09+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.2]": { - "last_validated_date": "2024-11-26T09:34:11+00:00" + "last_validated_date": "2025-03-31T12:22:34+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.3]": { - "last_validated_date": "2024-11-26T09:34:13+00:00" + "last_validated_date": "2025-03-31T12:22:37+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.4]": { + "last_validated_date": "2025-03-31T12:22:40+00:00" } } From 4447e30ee55c53bdded9eaccefde3d6d243fe6d7 Mon Sep 17 00:00:00 2001 From: Marco Dalla Santa <35923538+marcodallasanta@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:23:32 +0100 Subject: [PATCH 014/108] Step Functions: Improve the evaluation of JSONPath sampling using wildcards --- .../stepfunctions/asl/utils/json_path.py | 9 + .../stepfunctions/v2/base/test_base.py | 42 + .../v2/base/test_base.snapshot.json | 816 ++++++++++++++++++ .../v2/base/test_base.validation.json | 36 + 4 files changed, 903 insertions(+) diff --git a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py index 5345d53a225cc..2447458683daf 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py @@ -7,6 +7,7 @@ from localstack.services.events.utils import to_json_str _PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT: Final[str] = r"\[\d+\]$" +_PATTERN_SLICE_OR_WILDCARD_ACCESS = r"\$(?:\.[^[]+\[(?:\*|\d*:\d*)\]|\[\*\])(?:\.[^[]+)*$" def _is_singleton_array_access(path: str) -> bool: @@ -14,6 +15,12 @@ def _is_singleton_array_access(path: str) -> bool: return bool(re.search(_PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT, path)) +def _contains_slice_or_wildcard_array(path: str) -> bool: + # Returns true if the json path contains a slice or wildcard in the array. + # Slices at the root are discarded, but wildcard at the root is allowed. + return bool(re.search(_PATTERN_SLICE_OR_WILDCARD_ACCESS, path)) + + class NoSuchJsonPathError(Exception): json_path: Final[str] data: Final[Any] @@ -42,6 +49,8 @@ def extract_json(path: str, data: Any) -> Any: matches = input_expr.find(data) if not matches: + if _contains_slice_or_wildcard_array(path): + return [] raise NoSuchJsonPathError(json_path=path, data=data) if len(matches) > 1 or isinstance(matches[0].path, Index): diff --git a/tests/aws/services/stepfunctions/v2/base/test_base.py b/tests/aws/services/stepfunctions/v2/base/test_base.py index a85b40c818696..a124678cd42a5 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_base.py +++ b/tests/aws/services/stepfunctions/v2/base/test_base.py @@ -427,3 +427,45 @@ def test_json_path_array_access( definition, exec_input, ) + + # These json_path_strings are handled gracefully in AWS by returning an empty array, + # although there are some exceptions like "$[1:5]", "$[1:], "$[:1] + @markers.aws.validated + @pytest.mark.parametrize( + "json_path_string", + [ + "$[*]", + "$.items[*]", + "$.items[1:]", + "$.items[:1]", + "$.item.items[*]", + "$.item.items[1:]", + "$.item.items[:1]", + "$.item.items[1:5]", + "$.items[*].itemValue", + "$.items[1:].itemValue", + "$.items[:1].itemValue", + "$.item.items[1:5].itemValue", + ], + ) + def test_json_path_array_wildcard_or_slice_with_no_input( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + json_path_string, + ): + template = BaseTemplate.load_sfn_template(BaseTemplate.JSON_PATH_ARRAY_ACCESS) + template["States"]["EntryState"]["Parameters"]["item.$"] = json_path_string + definition = json.dumps(template) + + exec_input = json.dumps({}) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/aws/services/stepfunctions/v2/base/test_base.snapshot.json b/tests/aws/services/stepfunctions/v2/base/test_base.snapshot.json index a120eb1ddcb9d..9a1c0a80f39d3 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_base.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/base/test_base.snapshot.json @@ -1396,5 +1396,821 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$[*]]": { + "recorded-date": "01-04-2025, 20:52:06", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[*]]": { + "recorded-date": "01-04-2025, 20:52:20", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[1:]]": { + "recorded-date": "01-04-2025, 20:52:33", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[:1]]": { + "recorded-date": "01-04-2025, 20:52:46", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[*]]": { + "recorded-date": "01-04-2025, 20:52:58", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:]]": { + "recorded-date": "01-04-2025, 20:53:12", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[:1]]": { + "recorded-date": "01-04-2025, 20:53:25", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:5]]": { + "recorded-date": "01-04-2025, 20:53:38", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[*].itemValue]": { + "recorded-date": "01-04-2025, 20:53:51", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[1:].itemValue]": { + "recorded-date": "01-04-2025, 20:54:04", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[:1].itemValue]": { + "recorded-date": "01-04-2025, 20:54:18", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:5].itemValue]": { + "recorded-date": "01-04-2025, 20:54:31", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "EntryState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "EntryState", + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "item": [] + }, + "outputDetails": { + "truncated": false + } + }, + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/base/test_base.validation.json b/tests/aws/services/stepfunctions/v2/base/test_base.validation.json index 77f74c4d6dfcd..336b2b526c88a 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_base.validation.json +++ b/tests/aws/services/stepfunctions/v2/base/test_base.validation.json @@ -11,6 +11,42 @@ "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_access[$.items[10]]": { "last_validated_date": "2024-08-16T15:53:06+00:00" }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[*]]": { + "last_validated_date": "2025-04-01T20:52:58+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:5].itemValue]": { + "last_validated_date": "2025-04-01T20:54:31+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:5]]": { + "last_validated_date": "2025-04-01T20:53:38+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[1:]]": { + "last_validated_date": "2025-04-01T20:53:12+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.item.items[:1]]": { + "last_validated_date": "2025-04-01T20:53:25+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[*].itemValue]": { + "last_validated_date": "2025-04-01T20:53:51+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[*]]": { + "last_validated_date": "2025-04-01T20:52:20+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[1:].itemValue]": { + "last_validated_date": "2025-04-01T20:54:04+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[1:]]": { + "last_validated_date": "2025-04-01T20:52:33+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[:1].itemValue]": { + "last_validated_date": "2025-04-01T20:54:18+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$.items[:1]]": { + "last_validated_date": "2025-04-01T20:52:46+00:00" + }, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_json_path_array_wildcard_or_slice_with_no_input[$[*]]": { + "last_validated_date": "2025-04-01T20:52:06+00:00" + }, "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_query_context_object_values": { "last_validated_date": "2024-07-15T13:00:19+00:00" }, From b2456129015e230e73fe9423aa91918cf8860452 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 2 Apr 2025 10:57:52 +0200 Subject: [PATCH 015/108] Add Codeconnections to the client types (#12464) --- localstack-core/localstack/utils/aws/client_types.py | 4 ++++ pyproject.toml | 2 +- requirements-typehint.txt | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/utils/aws/client_types.py b/localstack-core/localstack/utils/aws/client_types.py index c0d48a92555a9..0c6e1ce4c7501 100644 --- a/localstack-core/localstack/utils/aws/client_types.py +++ b/localstack-core/localstack/utils/aws/client_types.py @@ -31,6 +31,7 @@ from mypy_boto3_cloudwatch import CloudWatchClient from mypy_boto3_codebuild import CodeBuildClient from mypy_boto3_codecommit import CodeCommitClient + from mypy_boto3_codeconnections import CodeConnectionsClient from mypy_boto3_codedeploy import CodeDeployClient from mypy_boto3_codepipeline import CodePipelineClient from mypy_boto3_codestar_connections import CodeStarconnectionsClient @@ -139,6 +140,9 @@ class TypedServiceClientFactory(abc.ABC): cloudwatch: Union["CloudWatchClient", "MetadataRequestInjector[CloudWatchClient]"] codebuild: Union["CodeBuildClient", "MetadataRequestInjector[CodeBuildClient]"] codecommit: Union["CodeCommitClient", "MetadataRequestInjector[CodeCommitClient]"] + codeconnections: Union[ + "CodeConnectionsClient", "MetadataRequestInjector[CodeConnectionsClient]" + ] codedeploy: Union["CodeDeployClient", "MetadataRequestInjector[CodeDeployClient]"] codepipeline: Union["CodePipelineClient", "MetadataRequestInjector[CodePipelineClient]"] codestar_connections: Union[ diff --git a/pyproject.toml b/pyproject.toml index af06383e3b382..9eaa72e897dc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codebuild,codecommit,codedeploy,codepipeline,codestar-connections,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,pinpoint,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codebuild,codecommit,codeconnections,codedeploy,codepipeline,codestar-connections,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,pinpoint,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]", ] [tool.setuptools] diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 3be4a0707597e..768a090aa10e0 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -302,6 +302,8 @@ mypy-boto3-codebuild==1.37.23 # via boto3-stubs mypy-boto3-codecommit==1.37.0 # via boto3-stubs +mypy-boto3-codeconnections==1.37.0 + # via boto3-stubs mypy-boto3-codedeploy==1.37.0 # via boto3-stubs mypy-boto3-codepipeline==1.37.0 @@ -696,6 +698,7 @@ typing-extensions==4.13.0 # mypy-boto3-cloudwatch # mypy-boto3-codebuild # mypy-boto3-codecommit + # mypy-boto3-codeconnections # mypy-boto3-codedeploy # mypy-boto3-codepipeline # mypy-boto3-codestar-connections From e1f34225ba2af55eb0d92605cb069a5ba6dc4107 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 2 Apr 2025 10:58:12 +0200 Subject: [PATCH 016/108] Fix lambda timeout race condition (#12465) --- .../services/lambda_/invocation/assignment.py | 6 +++-- .../invocation/execution_environment.py | 20 ++++++++++++---- .../localstack/services/lambda_/provider.py | 15 +++++++++--- tests/aws/services/lambda_/test_lambda.py | 24 +++++++------------ 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/invocation/assignment.py b/localstack-core/localstack/services/lambda_/invocation/assignment.py index 24cebeb7f8320..39f4d04383e26 100644 --- a/localstack-core/localstack/services/lambda_/invocation/assignment.py +++ b/localstack-core/localstack/services/lambda_/invocation/assignment.py @@ -86,7 +86,9 @@ def get_environment( except InvalidStatusException as invalid_e: LOG.error("InvalidStatusException: %s", invalid_e) except Exception as e: - LOG.error("Failed invocation %s", e) + LOG.error( + "Failed invocation <%s>: %s", type(e), e, exc_info=LOG.isEnabledFor(logging.DEBUG) + ) self.stop_environment(execution_environment) raise e @@ -107,7 +109,7 @@ def start_environment( except EnvironmentStartupTimeoutException: raise except Exception as e: - message = f"Could not start new environment: {e}" + message = f"Could not start new environment: {type(e).__name__}:{e}" raise AssignmentException(message) from e return execution_environment diff --git a/localstack-core/localstack/services/lambda_/invocation/execution_environment.py b/localstack-core/localstack/services/lambda_/invocation/execution_environment.py index bd65ba3904c69..139ec4d877fbe 100644 --- a/localstack-core/localstack/services/lambda_/invocation/execution_environment.py +++ b/localstack-core/localstack/services/lambda_/invocation/execution_environment.py @@ -37,10 +37,11 @@ class RuntimeStatus(Enum): INACTIVE = auto() STARTING = auto() READY = auto() - RUNNING = auto() + INVOKING = auto() STARTUP_FAILED = auto() STARTUP_TIMED_OUT = auto() STOPPED = auto() + TIMING_OUT = auto() class InvalidStatusException(Exception): @@ -246,7 +247,7 @@ def stop(self) -> None: def release(self) -> None: self.last_returned = datetime.now() with self.status_lock: - if self.status != RuntimeStatus.RUNNING: + if self.status != RuntimeStatus.INVOKING: raise InvalidStatusException( f"Execution environment {self.id} can only be set to status ready while running." f" Current status: {self.status}" @@ -264,7 +265,7 @@ def reserve(self) -> None: f"Execution environment {self.id} can only be reserved if ready. " f" Current status: {self.status}" ) - self.status = RuntimeStatus.RUNNING + self.status = RuntimeStatus.INVOKING self.keepalive_timer.cancel() @@ -274,6 +275,17 @@ def keepalive_passed(self) -> None: self.id, self.function_version.qualified_arn, ) + # The stop() method allows to interrupt invocations (on purpose), which might cancel running invocations + # which we should not do when the keepalive timer passed. + # The new TIMING_OUT state prevents this race condition + with self.status_lock: + if self.status != RuntimeStatus.READY: + LOG.debug( + "Keepalive timer passed, but current runtime status is %s. Aborting keepalive stop.", + self.status, + ) + return + self.status = RuntimeStatus.TIMING_OUT self.stop() # Notify assignment service via callback to remove from environments list self.on_timeout(self.version_manager_id, self.id) @@ -340,7 +352,7 @@ def get_prefixed_logs(self) -> str: return f"{prefix}{prefixed_logs}" def invoke(self, invocation: Invocation) -> InvocationResult: - assert self.status == RuntimeStatus.RUNNING + assert self.status == RuntimeStatus.INVOKING # Async/event invokes might miss an aws_trace_header, then we need to create a new root trace id. aws_trace_header = ( invocation.trace_context.get("aws_trace_header") or TraceHeader().ensure_root_exists() diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index 54a21f9047d9c..a30b8be7afc59 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -1603,10 +1603,19 @@ def invoke( except ServiceException: raise except EnvironmentStartupTimeoutException as e: - raise LambdaServiceException("Internal error while executing lambda") from e + raise LambdaServiceException( + f"[{context.request_id}] Timeout while starting up lambda environment for function {function_name}:{qualifier}" + ) from e except Exception as e: - LOG.error("Error while invoking lambda", exc_info=e) - raise LambdaServiceException("Internal error while executing lambda") from e + LOG.error( + "[%s] Error while invoking lambda %s", + context.request_id, + function_name, + exc_info=LOG.isEnabledFor(logging.DEBUG), + ) + raise LambdaServiceException( + f"[{context.request_id}] Internal error while executing lambda {function_name}:{qualifier}. Caused by {type(e).__name__}: {e}" + ) from e if invocation_type == InvocationType.Event: # This happens when invocation type is event diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index cba8061226134..ad50d73d258e7 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -1908,7 +1908,7 @@ def test_lambda_runtime_wrapper_not_found(self, aws_client, create_lambda_functi reason="Can only induce Lambda-internal Docker error in LocalStack" ) def test_lambda_runtime_startup_timeout( - self, aws_client_factory, create_lambda_function, monkeypatch + self, aws_client_no_retry, create_lambda_function, monkeypatch ): """Test Lambda that times out during runtime startup""" monkeypatch.setattr( @@ -1924,24 +1924,20 @@ def test_lambda_runtime_startup_timeout( runtime=Runtime.python3_12, ) - client_config = Config( - retries={"max_attempts": 0}, - ) - no_retry_lambda_client = aws_client_factory.get_client("lambda", config=client_config) - with pytest.raises(no_retry_lambda_client.exceptions.ServiceException) as e: - no_retry_lambda_client.invoke( + with pytest.raises(aws_client_no_retry.lambda_.exceptions.ServiceException) as e: + aws_client_no_retry.lambda_.invoke( FunctionName=function_name, ) assert e.match( r"An error occurred \(ServiceException\) when calling the Invoke operation \(reached max " - r"retries: \d\): Internal error while executing lambda" + r"retries: \d\): \[[^]]*\] Timeout while starting up lambda environment .*" ) @markers.aws.only_localstack( reason="Can only induce Lambda-internal Docker error in LocalStack" ) def test_lambda_runtime_startup_error( - self, aws_client_factory, create_lambda_function, monkeypatch + self, aws_client_no_retry, create_lambda_function, monkeypatch ): """Test Lambda that errors during runtime startup""" monkeypatch.setattr(config, "LAMBDA_DOCKER_FLAGS", "invalid_flags") @@ -1954,17 +1950,13 @@ def test_lambda_runtime_startup_error( runtime=Runtime.python3_12, ) - client_config = Config( - retries={"max_attempts": 0}, - ) - no_retry_lambda_client = aws_client_factory.get_client("lambda", config=client_config) - with pytest.raises(no_retry_lambda_client.exceptions.ServiceException) as e: - no_retry_lambda_client.invoke( + with pytest.raises(aws_client_no_retry.lambda_.exceptions.ServiceException) as e: + aws_client_no_retry.lambda_.invoke( FunctionName=function_name, ) assert e.match( r"An error occurred \(ServiceException\) when calling the Invoke operation \(reached max " - r"retries: \d\): Internal error while executing lambda" + r"retries: \d\): \[[^]]*\] Internal error while executing lambda" ) @markers.aws.validated From 245ebe0625ae07527ec70d45b94b2154e2fe28ef Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Wed, 2 Apr 2025 14:40:02 +0100 Subject: [PATCH 017/108] CFn: WIP POC v2 executor (#12396) Co-authored-by: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> --- .../cloudformation/engine/entities.py | 19 +- .../engine/template_deployer.py | 9 - .../engine/v2/change_set_model.py | 22 ++- .../engine/v2/change_set_model_describer.py | 10 +- .../engine/v2/change_set_model_executor.py | 170 ++++++++++++++++++ .../services/cloudformation/v2/provider.py | 78 +++++++- .../resource_providers/aws_ssm_parameter.py | 6 +- .../cloudformation/api/test_changesets.py | 126 +++++++++++++ .../api/test_changesets.validation.json | 3 + .../cloudformation/v2/test_change_sets.py | 91 ++++++++++ .../v2/test_change_sets.snapshot.json | 97 ++++++++++ .../v2/test_change_sets.validation.json | 5 + 12 files changed, 607 insertions(+), 29 deletions(-) create mode 100644 localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_sets.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_sets.validation.json diff --git a/localstack-core/localstack/services/cloudformation/engine/entities.py b/localstack-core/localstack/services/cloudformation/engine/entities.py index cd2a2517432fd..3df7f8ea19195 100644 --- a/localstack-core/localstack/services/cloudformation/engine/entities.py +++ b/localstack-core/localstack/services/cloudformation/engine/entities.py @@ -297,6 +297,10 @@ def resources(self): """Return dict of resources""" return dict(self.template_resources) + @resources.setter + def resources(self, resources: dict): + self.template["Resources"] = resources + @property def template_resources(self): return self.template.setdefault("Resources", {}) @@ -370,8 +374,17 @@ def copy(self): # TODO: what functionality of the Stack object do we rely on here? class StackChangeSet(Stack): update_graph: NodeTemplate | None + change_set_type: ChangeSetType | None - def __init__(self, account_id: str, region_name: str, stack: Stack, params=None, template=None): + def __init__( + self, + account_id: str, + region_name: str, + stack: Stack, + params=None, + template=None, + change_set_type: ChangeSetType | None = None, + ): if template is None: template = {} if params is None: @@ -389,6 +402,7 @@ def __init__(self, account_id: str, region_name: str, stack: Stack, params=None, self.stack = stack self.metadata["StackId"] = stack.stack_id self.metadata["Status"] = "CREATE_PENDING" + self.change_set_type = change_set_type @property def change_set_id(self): @@ -412,5 +426,8 @@ def populate_update_graph(self, before_template: dict | None, after_template: di change_set_model = ChangeSetModel( before_template=before_template, after_template=after_template, + # TODO + before_parameters=None, + after_parameters=None, ) self.update_graph = change_set_model.get_update_model() diff --git a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py index 5bfcf02c5453a..a0ae9c286d61c 100644 --- a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py @@ -1409,15 +1409,6 @@ def delete_stack(self): ) # TODO: why is there a fallback? resource["ResourceType"] = get_resource_type(resource) - def _safe_lookup_is_deleted(r_id): - """handles the case where self.stack.resource_status(..) fails for whatever reason""" - try: - return self.stack.resource_status(r_id).get("ResourceStatus") == "DELETE_COMPLETE" - except Exception: - if config.CFN_VERBOSE_ERRORS: - LOG.exception("failed to lookup if resource %s is deleted", r_id) - return True # just an assumption - ordered_resource_ids = list( order_resources( resources=original_resources, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 8995624c50d7c..a9a3fdc4fe15d 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -7,6 +7,7 @@ from typing_extensions import TypeVar +from localstack.aws.api.cloudformation import ChangeAction from localstack.utils.strings import camel_to_snake_case T = TypeVar("T") @@ -66,6 +67,15 @@ class ChangeType(enum.Enum): def __str__(self): return self.value + def to_action(self) -> ChangeAction | None: + match self: + case self.CREATED: + return ChangeAction.Add + case self.MODIFIED: + return ChangeAction.Modify + case self.REMOVED: + return ChangeAction.Remove + def for_child(self, child_change_type: ChangeType) -> ChangeType: if child_change_type == self: return self @@ -686,24 +696,24 @@ def _visit_property( if isinstance(node_property, NodeProperty): return node_property + value = self._visit_value( + scope=scope, before_value=before_property, after_value=after_property + ) if self._is_created(before=before_property, after=after_property): node_property = NodeProperty( scope=scope, change_type=ChangeType.CREATED, name=property_name, - value=TerminalValueCreated(scope=scope, value=after_property), + value=value, ) elif self._is_removed(before=before_property, after=after_property): node_property = NodeProperty( scope=scope, change_type=ChangeType.REMOVED, name=property_name, - value=TerminalValueRemoved(scope=scope, value=before_property), + value=value, ) else: - value = self._visit_value( - scope=scope, before_value=before_property, after_value=after_property - ) node_property = NodeProperty( scope=scope, change_type=value.change_type, name=property_name, value=value ) @@ -757,7 +767,7 @@ def _visit_resource( change_type = ChangeType.UNCHANGED # TODO: investigate behaviour with type changes, for now this is filler code. - _, type_str = self._safe_access_in(scope, TypeKey, before_resource) + _, type_str = self._safe_access_in(scope, TypeKey, after_resource) condition_reference = None scope_condition, (before_condition, after_condition) = self._safe_access_in( diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index 4e9dcdd6369e2..7b114fed93a66 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -52,14 +52,20 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor): _node_template: Final[NodeTemplate] _changes: Final[cfn_api.Changes] _describe_unit_cache: dict[Scope, DescribeUnit] + _include_property_values: Final[cfn_api.IncludePropertyValues | None] - def __init__(self, node_template: NodeTemplate): + def __init__( + self, + node_template: NodeTemplate, + include_property_values: cfn_api.IncludePropertyValues | None = None, + ): self._node_template = node_template self._changes = list() self._describe_unit_cache = dict() - self.visit(self._node_template) + self._include_property_values = include_property_values def get_changes(self) -> cfn_api.Changes: + self.visit(self._node_template) return self._changes @staticmethod diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py new file mode 100644 index 0000000000000..cd162e9c77d57 --- /dev/null +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -0,0 +1,170 @@ +import logging +import uuid +from typing import Final + +from localstack.aws.api.cloudformation import ChangeAction +from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY +from localstack.services.cloudformation.engine.v2.change_set_model import ( + NodeIntrinsicFunction, + NodeResource, + NodeTemplate, + TerminalValue, +) +from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( + ChangeSetModelDescriber, + DescribeUnit, +) +from localstack.services.cloudformation.resource_provider import ( + Credentials, + OperationStatus, + ProgressEvent, + ResourceProviderExecutor, + ResourceProviderPayload, + get_resource_type, +) + +LOG = logging.getLogger(__name__) + + +class ChangeSetModelExecutor(ChangeSetModelDescriber): + account_id: Final[str] + region: Final[str] + + def __init__( + self, + node_template: NodeTemplate, + account_id: str, + region: str, + stack_name: str, + stack_id: str, + ): + super().__init__(node_template) + self.account_id = account_id + self.region = region + self.stack_name = stack_name + self.stack_id = stack_id + self.resources = {} + + def execute(self) -> dict: + self.visit(self._node_template) + return self.resources + + def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: + resource_provider_executor = ResourceProviderExecutor( + stack_name=self.stack_name, stack_id=self.stack_id + ) + + # TODO: investigate effects on type changes + properties_describe_unit = self.visit_node_properties(node_resource.properties) + LOG.info("SRW: describe unit: %s", properties_describe_unit) + + action = node_resource.change_type.to_action() + if action is None: + raise RuntimeError( + f"Action should always be present, got change type: {node_resource.change_type}" + ) + + # TODO + resource_type = get_resource_type({"Type": "AWS::SSM::Parameter"}) + payload = self.create_resource_provider_payload( + properties_describe_unit, + action, + node_resource.name, + resource_type, + ) + resource_provider = resource_provider_executor.try_load_resource_provider(resource_type) + + extra_resource_properties = {} + if resource_provider is not None: + # TODO: stack events + event = resource_provider_executor.deploy_loop( + resource_provider, extra_resource_properties, payload + ) + else: + event = ProgressEvent(OperationStatus.SUCCESS, resource_model={}) + + self.resources.setdefault(node_resource.name, {"Properties": {}}) + match event.status: + case OperationStatus.SUCCESS: + # merge the resources state with the external state + # TODO: this is likely a duplicate of updating from extra_resource_properties + self.resources[node_resource.name]["Properties"].update(event.resource_model) + self.resources[node_resource.name].update(extra_resource_properties) + # XXX for legacy delete_stack compatibility + self.resources[node_resource.name]["LogicalResourceId"] = node_resource.name + self.resources[node_resource.name]["Type"] = resource_type + case any: + raise NotImplementedError(f"Event status '{any}' not handled") + + return DescribeUnit(before_context=None, after_context={}) + + def visit_node_intrinsic_function_fn_get_att( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> DescribeUnit: + arguments_unit = self.visit(node_intrinsic_function.arguments) + before_arguments_list = arguments_unit.before_context + after_arguments_list = arguments_unit.after_context + if before_arguments_list: + logical_name_of_resource = before_arguments_list[0] + attribute_name = before_arguments_list[1] + before_node_resource = self._get_node_resource_for( + resource_name=logical_name_of_resource, node_template=self._node_template + ) + node_property: TerminalValue = self._get_node_property_for( + property_name=attribute_name, node_resource=before_node_resource + ) + before_context = self.visit(node_property.value).before_context + else: + before_context = None + + if after_arguments_list: + logical_name_of_resource = after_arguments_list[0] + attribute_name = after_arguments_list[1] + after_node_resource = self._get_node_resource_for( + resource_name=logical_name_of_resource, node_template=self._node_template + ) + node_property: TerminalValue = self._get_node_property_for( + property_name=attribute_name, node_resource=after_node_resource + ) + after_context = self.visit(node_property.value).after_context + else: + after_context = None + + return DescribeUnit(before_context=before_context, after_context=after_context) + + def create_resource_provider_payload( + self, + describe_unit: DescribeUnit, + action: ChangeAction, + logical_resource_id: str, + resource_type: str, + ) -> ResourceProviderPayload: + # FIXME: use proper credentials + creds: Credentials = { + "accessKeyId": self.account_id, + "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY, + "sessionToken": "", + } + resource_provider_payload: ResourceProviderPayload = { + "awsAccountId": self.account_id, + "callbackContext": {}, + "stackId": self.stack_name, + "resourceType": resource_type, + "resourceTypeVersion": "000000", + # TODO: not actually a UUID + "bearerToken": str(uuid.uuid4()), + "region": self.region, + "action": str(action), + "requestData": { + "logicalResourceId": logical_resource_id, + "resourceProperties": describe_unit.after_context["Properties"], + "previousResourceProperties": describe_unit.before_context["Properties"], + "callerCredentials": creds, + "providerCredentials": creds, + "systemTags": {}, + "previousSystemTags": {}, + "stackTags": {}, + "previousStackTags": {}, + }, + } + return resource_provider_payload diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 9017a0c696f51..b713da7ffe670 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -1,3 +1,4 @@ +import logging from copy import deepcopy from localstack.aws.api import RequestContext, handler @@ -5,12 +6,18 @@ ChangeSetNameOrId, ChangeSetNotFoundException, ChangeSetType, + ClientRequestToken, CreateChangeSetInput, CreateChangeSetOutput, DescribeChangeSetOutput, + DisableRollback, + ExecuteChangeSetOutput, + ExecutionStatus, IncludePropertyValues, + InvalidChangeSetStatusException, NextToken, Parameter, + RetainExceptOnCreate, StackNameOrId, StackStatus, ) @@ -27,6 +34,9 @@ from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( ChangeSetModelDescriber, ) +from localstack.services.cloudformation.engine.v2.change_set_model_executor import ( + ChangeSetModelExecutor, +) from localstack.services.cloudformation.engine.validations import ValidationError from localstack.services.cloudformation.provider import ( ARN_CHANGESET_REGEX, @@ -41,6 +51,8 @@ ) from localstack.utils.collections import remove_attributes +LOG = logging.getLogger(__name__) + class CloudformationProviderV2(CloudformationProvider): @handler("CreateChangeSet", expand=False) @@ -178,7 +190,12 @@ def create_change_set( # create change set for the stack and apply changes change_set = StackChangeSet( - context.account_id, context.region, stack, req_params, transformed_template + context.account_id, + context.region, + stack, + req_params, + transformed_template, + change_set_type=change_set_type, ) # only set parameters for the changeset, then switch to stack on execute_change_set change_set.template_body = template_body @@ -233,14 +250,61 @@ def create_change_set( return CreateChangeSetOutput(StackId=change_set.stack_id, Id=change_set.change_set_id) + @handler("ExecuteChangeSet") + def execute_change_set( + self, + context: RequestContext, + change_set_name: ChangeSetNameOrId, + stack_name: StackNameOrId | None = None, + client_request_token: ClientRequestToken | None = None, + disable_rollback: DisableRollback | None = None, + retain_except_on_create: RetainExceptOnCreate | None = None, + **kwargs, + ) -> ExecuteChangeSetOutput: + change_set = find_change_set( + context.account_id, + context.region, + change_set_name, + stack_name=stack_name, + active_only=True, + ) + if not change_set: + raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") + if change_set.metadata.get("ExecutionStatus") != ExecutionStatus.AVAILABLE: + LOG.debug("Change set %s not in execution status 'AVAILABLE'", change_set_name) + raise InvalidChangeSetStatusException( + f"ChangeSet [{change_set.metadata['ChangeSetId']}] cannot be executed in its current status of [{change_set.metadata.get('Status')}]" + ) + stack_name = change_set.stack.stack_name + LOG.debug( + 'Executing change set "%s" for stack "%s" with %s resources ...', + change_set_name, + stack_name, + len(change_set.template_resources), + ) + if not change_set.update_graph: + raise RuntimeError("Programming error: no update graph found for change set") + + change_set_executor = ChangeSetModelExecutor( + change_set.update_graph, + account_id=context.account_id, + region=context.region, + stack_name=change_set.stack.stack_name, + stack_id=change_set.stack.stack_id, + ) + new_resources = change_set_executor.execute() + change_set.stack.set_stack_status(f"{change_set.change_set_type or 'UPDATE'}_COMPLETE") + change_set.stack.resources = new_resources + return ExecuteChangeSetOutput() + @handler("DescribeChangeSet") def describe_change_set( self, context: RequestContext, change_set_name: ChangeSetNameOrId, - stack_name: StackNameOrId = None, - next_token: NextToken = None, - include_property_values: IncludePropertyValues = None, + stack_name: StackNameOrId | None = None, + next_token: NextToken | None = None, + include_property_values: IncludePropertyValues | None = None, **kwargs, ) -> DescribeChangeSetOutput: # TODO add support for include_property_values @@ -261,8 +325,10 @@ def describe_change_set( if not change_set: raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") - change_set_describer = ChangeSetModelDescriber(node_template=change_set.update_graph) - resource_changes = change_set_describer.get_resource_changes() + change_set_describer = ChangeSetModelDescriber( + node_template=change_set.update_graph, include_property_values=include_property_values + ) + resource_changes = change_set_describer.get_changes() attrs = [ "ChangeSetType", diff --git a/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py b/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py index 16c9109270926..42e834f59ff53 100644 --- a/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py +++ b/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py @@ -177,11 +177,7 @@ def update( ssm.put_parameter(Overwrite=True, Tags=[], **update_config_props) - return ProgressEvent( - status=OperationStatus.SUCCESS, - resource_model=model, - custom_context=request.custom_context, - ) + return self.read(request) def update_tags(self, ssm, model, new_tags): current_tags = ssm.list_tags_for_resource( diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index c03b9e22b8d92..86661fda10bd9 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -1,3 +1,4 @@ +import copy import json import os.path from collections import defaultdict @@ -28,6 +29,131 @@ def is_v2_engine() -> bool: return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2" +class TestUpdates: + @markers.aws.validated + def test_simple_update_single_resource( + self, aws_client: ServiceLevelClientFactory, deploy_cfn_template + ): + parameter_name = "my-parameter" + value1 = "foo" + value2 = "bar" + stack_name = f"stack-{short_uid()}" + + t1 = { + "Resources": { + "MyParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name, + "Type": "String", + "Value": value1, + }, + }, + }, + } + + res = deploy_cfn_template(stack_name=stack_name, template=json.dumps(t1), is_update=False) + + found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + assert found_value == value1 + + t2 = copy.deepcopy(t1) + t2["Resources"]["MyParameter"]["Properties"]["Value"] = value2 + + deploy_cfn_template(stack_name=stack_name, template=json.dumps(t2), is_update=True) + found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + assert found_value == value2 + + res.destroy() + + # @pytest.mark.skipif( + # condition=not is_v2_engine() and not is_aws_cloud(), reason="Not working in v2 yet" + # ) + @markers.aws.validated + def test_simple_update_two_resources( + self, aws_client: ServiceLevelClientFactory, deploy_cfn_template + ): + parameter_name = "my-parameter" + value1 = "foo" + value2 = "bar" + stack_name = f"stack-{short_uid()}" + + t1 = { + "Resources": { + "MyParameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": value1, + }, + }, + "MyParameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name, + "Type": "String", + "Value": {"Fn::GetAtt": ["MyParameter1", "Value"]}, + }, + }, + }, + } + + res = deploy_cfn_template(stack_name=stack_name, template=json.dumps(t1), is_update=False) + found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + assert found_value == value1 + + t2 = copy.deepcopy(t1) + t2["Resources"]["MyParameter1"]["Properties"]["Value"] = value2 + + deploy_cfn_template(stack_name=stack_name, template=json.dumps(t2), is_update=True) + found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + assert found_value == value2 + + res.destroy() + + @markers.aws.needs_fixing + @pytest.mark.skip(reason="WIP") + def test_deleting_resource(self, aws_client: ServiceLevelClientFactory, deploy_cfn_template): + parameter_name = "my-parameter" + value1 = "foo" + stack_name = f"stack-{short_uid()}" + + t1 = { + "Resources": { + "MyParameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": value1, + }, + }, + "MyParameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name, + "Type": "String", + "Value": {"Fn::GetAtt": ["MyParameter1", "Value"]}, + }, + }, + }, + } + + res = deploy_cfn_template(stack_name=stack_name, template=json.dumps(t1), is_update=False) + found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + assert found_value == value1 + + t2 = copy.deepcopy(t1) + del t2["Resources"]["MyParameter2"] + + deploy_cfn_template(stack_name=stack_name, template=json.dumps(t2), is_update=True) + with pytest.raises(ClientError) as exc_info: + aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] + + assert f"Parameter {parameter_name} not found" in str(exc_info.value) + + res.destroy() + + @markers.aws.validated def test_create_change_set_without_parameters( cleanup_stacks, cleanup_changesets, is_change_set_created_and_available, aws_client diff --git a/tests/aws/services/cloudformation/api/test_changesets.validation.json b/tests/aws/services/cloudformation/api/test_changesets.validation.json index 949d5bbad61a3..60cb1602919a3 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.validation.json +++ b/tests/aws/services/cloudformation/api/test_changesets.validation.json @@ -23,6 +23,9 @@ "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { "last_validated_date": "2025-03-27T15:34:51+00:00" }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_simple_update_two_resources": { + "last_validated_date": "2025-04-02T10:05:26+00:00" + }, "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_update_without_parameters": { "last_validated_date": "2022-05-31T07:32:02+00:00" }, diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.py b/tests/aws/services/cloudformation/v2/test_change_sets.py new file mode 100644 index 0000000000000..3a86ac9cbbeeb --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_sets.py @@ -0,0 +1,91 @@ +import copy +import json + +import pytest + +from localstack import config +from localstack.aws.connect import ServiceLevelClientFactory +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.utils.strings import short_uid + +pytestmark = pytest.mark.skipif( + not is_aws_cloud() and config.SERVICE_PROVIDER_CONFIG["cloudformation"] == "engine-v2", + reason="Only targeting the new engine", +) + + +@markers.aws.validated +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..Capabilities", + "$..IncludeNestedStacks", + "$..NotificationARNs", + "$..Parameters", + "$..Changes..ResourceChange.Details", + "$..Changes..ResourceChange.Scope", + "$..Changes..ResourceChange.PhysicalResourceId", + "$..Changes..ResourceChange.Replacement", + ] +) +def test_single_resource_static_update(aws_client: ServiceLevelClientFactory, snapshot, cleanups): + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + parameter_name = f"parameter-{short_uid()}" + value1 = "foo" + value2 = "bar" + + t1 = { + "Resources": { + "MyParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name, + "Type": "String", + "Value": value1, + }, + }, + }, + } + + stack_name = f"stack-{short_uid()}" + change_set_name = f"cs-{short_uid()}" + cs_result = aws_client.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=json.dumps(t1), + ChangeSetType="CREATE", + ) + cs_id = cs_result["Id"] + stack_id = cs_result["StackId"] + aws_client.cloudformation.get_waiter("change_set_create_complete").wait(ChangeSetName=cs_id) + cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id)) + + describe_result = aws_client.cloudformation.describe_change_set(ChangeSetName=cs_id) + snapshot.match("describe-1", describe_result) + + aws_client.cloudformation.execute_change_set(ChangeSetName=cs_id) + aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_id) + + parameter = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"] + snapshot.match("parameter-1", parameter) + + t2 = copy.deepcopy(t1) + t2["Resources"]["MyParameter"]["Properties"]["Value"] = value2 + + change_set_name = f"cs-{short_uid()}" + cs_result = aws_client.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=json.dumps(t2), + ) + cs_id = cs_result["Id"] + aws_client.cloudformation.get_waiter("change_set_create_complete").wait(ChangeSetName=cs_id) + + describe_result = aws_client.cloudformation.describe_change_set(ChangeSetName=cs_id) + snapshot.match("describe-2", describe_result) + + aws_client.cloudformation.execute_change_set(ChangeSetName=cs_id) + aws_client.cloudformation.get_waiter("stack_update_complete").wait(StackName=stack_id) + + parameter = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"] + snapshot.match("parameter-2", parameter) diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json new file mode 100644 index 0000000000000..5226f24647715 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json @@ -0,0 +1,97 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_sets.py::test_single_resource_static_update": { + "recorded-date": "18-03-2025, 16:52:36", + "recorded-content": { + "describe-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "MyParameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "parameter-1": { + "ARN": "arn::ssm::111111111111:parameter/", + "DataType": "text", + "LastModifiedDate": "datetime", + "Name": "", + "Type": "String", + "Value": "foo", + "Version": 1 + }, + "describe-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "MyParameter", + "PhysicalResourceId": "", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "parameter-2": { + "ARN": "arn::ssm::111111111111:parameter/", + "DataType": "text", + "LastModifiedDate": "datetime", + "Name": "", + "Type": "String", + "Value": "bar", + "Version": 2 + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.validation.json b/tests/aws/services/cloudformation/v2/test_change_sets.validation.json new file mode 100644 index 0000000000000..db75052b3a4d9 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_sets.validation.json @@ -0,0 +1,5 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_sets.py::test_single_resource_static_update": { + "last_validated_date": "2025-03-18T16:52:35+00:00" + } +} From e86e0c4dcddee03cf548d91facbe230f12f8ce8c Mon Sep 17 00:00:00 2001 From: Misha Tiurin <650819+tiurin@users.noreply.github.com> Date: Thu, 3 Apr 2025 09:06:46 +0200 Subject: [PATCH 018/108] Skip flaky transcribe tests (#12473) --- tests/aws/services/transcribe/test_transcribe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/aws/services/transcribe/test_transcribe.py b/tests/aws/services/transcribe/test_transcribe.py index 572b1b0a4c0b1..d52ac48c7e886 100644 --- a/tests/aws/services/transcribe/test_transcribe.py +++ b/tests/aws/services/transcribe/test_transcribe.py @@ -138,6 +138,7 @@ def is_transcription_done(): "$..Error..Code", ] ) + @pytest.mark.skip(reason="flaky") def test_transcribe_happy_path(self, transcribe_create_job, snapshot, aws_client): file_path = os.path.join(BASEDIR, "../../files/en-gb.wav") job_name = transcribe_create_job(audio_file=file_path) @@ -182,6 +183,7 @@ def is_transcription_done(): ], ) @markers.aws.needs_fixing + @pytest.mark.skip(reason="flaky") def test_transcribe_supported_media_formats( self, transcribe_create_job, media_file, speech, aws_client ): @@ -322,6 +324,7 @@ def test_failing_start_transcription_job(self, s3_bucket, snapshot, aws_client): (None, None), # without output bucket and output key ], ) + @pytest.mark.skip(reason="flaky") def test_transcribe_start_job( self, output_bucket, From 997bcffdbb01256d85ff85c7c8b635bca0c278a5 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:03:14 +0200 Subject: [PATCH 019/108] Step Functions: Migrate v2 Test Suite to no_retry aws_client Fixture (#12461) --- .../stepfunctions/v2/base/test_wait.py | 7 +- .../v2/callback/test_callback.py | 3 +- .../v2/credentials/test_credentials_base.py | 3 +- .../stepfunctions/v2/logs/test_logs.py | 7 +- .../v2/scenarios/test_base_scenarios.py | 8 +- .../v2/services/test_dynamodb_task_service.py | 4 +- .../services/stepfunctions/v2/test_sfn_api.py | 196 +++++++++++++----- .../v2/test_sfn_api_activities.py | 26 +-- .../stepfunctions/v2/test_sfn_api_aliasing.py | 34 +-- .../stepfunctions/v2/test_sfn_api_express.py | 15 +- .../stepfunctions/v2/test_sfn_api_logs.py | 16 +- .../stepfunctions/v2/test_sfn_api_map_run.py | 12 +- .../stepfunctions/v2/test_sfn_api_tagging.py | 8 +- .../v2/test_sfn_api_variable_references.py | 10 +- .../v2/test_sfn_api_versioning.py | 90 ++++++-- 15 files changed, 303 insertions(+), 136 deletions(-) diff --git a/tests/aws/services/stepfunctions/v2/base/test_wait.py b/tests/aws/services/stepfunctions/v2/base/test_wait.py index 43475c6dfee92..b9c1f4a243b2e 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_wait.py +++ b/tests/aws/services/stepfunctions/v2/base/test_wait.py @@ -18,7 +18,12 @@ class TestSfnWait: @markers.aws.validated @pytest.mark.parametrize("days", [24855, 24856]) def test_timestamp_too_far_in_future_boundary( - self, aws_client, create_state_machine_iam_role, create_state_machine, sfn_snapshot, days + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + days, ): """ seems this seems to correlate with "2147483648" as the maximum integer value for the seconds stepfunctions internally uses to represent dates diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index 7a3d7000837bb..90879273d2d28 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -529,7 +529,8 @@ def test_multiple_heartbeat_notifications( sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs_queue_name")) task_token_consumer_thread = threading.Thread( - target=_handle_sqs_task_token_with_heartbeats_and_success, args=(aws_client, queue_url) + target=_handle_sqs_task_token_with_heartbeats_and_success, + args=(aws_client, queue_url), ) task_token_consumer_thread.start() diff --git a/tests/aws/services/stepfunctions/v2/credentials/test_credentials_base.py b/tests/aws/services/stepfunctions/v2/credentials/test_credentials_base.py index 201c0a7f70904..4381de35a3ef3 100644 --- a/tests/aws/services/stepfunctions/v2/credentials/test_credentials_base.py +++ b/tests/aws/services/stepfunctions/v2/credentials/test_credentials_base.py @@ -45,6 +45,7 @@ class TestCredentialsBase: def test_invalid_credentials_field( self, aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -60,7 +61,7 @@ def test_invalid_credentials_field( with pytest.raises(Exception) as ex: create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn + aws_client_no_retry, name=sm_name, definition=definition_str, roleArn=snf_role_arn ) sfn_snapshot.match("invalid_definition", ex.value.response) diff --git a/tests/aws/services/stepfunctions/v2/logs/test_logs.py b/tests/aws/services/stepfunctions/v2/logs/test_logs.py index 098b220010361..0948cdbb54fd5 100644 --- a/tests/aws/services/stepfunctions/v2/logs/test_logs.py +++ b/tests/aws/services/stepfunctions/v2/logs/test_logs.py @@ -142,7 +142,12 @@ def test_partial_log_levels( execution_input = json.dumps({}) launch_and_record_logs( - aws_client, state_machine_arn, execution_input, log_level, log_group_name, sfn_snapshot + aws_client, + state_machine_arn, + execution_input, + log_level, + log_group_name, + sfn_snapshot, ) @markers.aws.validated diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index 92569e906ce3a..568ab840aafbb 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -2515,7 +2515,7 @@ def test_wait_timestamp( ) def test_wait_timestamp_invalid( self, - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -2526,7 +2526,7 @@ def test_wait_timestamp_invalid( definition = json.dumps(template) with pytest.raises(Exception) as ex: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -2819,7 +2819,7 @@ def test_escape_sequence_parsing( ) def test_illegal_escapes( self, - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -2829,7 +2829,7 @@ def test_illegal_escapes( definition = json.dumps(template) with pytest.raises(Exception) as ex: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, diff --git a/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py index 329fd3182ca39..b198625b0f1b4 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py @@ -76,7 +76,7 @@ def test_base_integrations( @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) def test_invalid_integration( self, - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -85,7 +85,7 @@ def test_invalid_integration( definition = json.dumps(template) with pytest.raises(Exception) as ex: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.py b/tests/aws/services/stepfunctions/v2/test_sfn_api.py index 285ea25db3042..b46ec48bd3361 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.py @@ -77,7 +77,12 @@ def test_create_delete_valid_sm( ) @markers.aws.validated def test_create_delete_invalid_sm( - self, aws_client, create_state_machine_iam_role, create_state_machine, sfn_snapshot + self, + aws_client, + aws_client_no_retry, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -89,7 +94,7 @@ def test_create_delete_invalid_sm( with pytest.raises(Exception) as resource_not_found: create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn + aws_client_no_retry, name=sm_name, definition=definition_str, roleArn=snf_role_arn ) sfn_snapshot.match("invalid_definition_1", resource_not_found.value.response) @@ -119,7 +124,12 @@ def test_delete_nonexistent_sm( @markers.aws.validated def test_describe_nonexistent_sm( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -138,7 +148,9 @@ def test_describe_nonexistent_sm( sfn_snapshot.add_transformer(RegexTransformer(sm_nonexistent_arn, "sm_nonexistent_arn")) with pytest.raises(Exception) as exc: - aws_client.stepfunctions.describe_state_machine(stateMachineArn=sm_nonexistent_arn) + aws_client_no_retry.stepfunctions.describe_state_machine( + stateMachineArn=sm_nonexistent_arn + ) sfn_snapshot.match("describe_nonexistent_sm", exc.value) @markers.aws.validated @@ -167,9 +179,11 @@ def test_describe_sm_arn_containing_punctuation( @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) - def test_describe_invalid_arn_sm(self, sfn_snapshot, aws_client): + def test_describe_invalid_arn_sm(self, sfn_snapshot, aws_client_no_retry): with pytest.raises(Exception) as exc: - aws_client.stepfunctions.describe_state_machine(stateMachineArn="not_a_valid_arn") + aws_client_no_retry.stepfunctions.describe_state_machine( + stateMachineArn="not_a_valid_arn" + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @@ -216,7 +230,12 @@ def test_create_exact_duplicate_sm( @markers.aws.validated def test_create_duplicate_definition_format_sm( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -240,13 +259,18 @@ def test_create_duplicate_definition_format_sm( definition_str_2 = json.dumps(definition, indent=4) with pytest.raises(Exception) as resource_not_found: create_state_machine( - aws_client, name=sm_name, definition=definition_str_2, roleArn=snf_role_arn + aws_client_no_retry, name=sm_name, definition=definition_str_2, roleArn=snf_role_arn ) sfn_snapshot.match("already_exists_1", resource_not_found.value.response) @markers.aws.validated def test_create_duplicate_sm_name( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -273,7 +297,7 @@ def test_create_duplicate_sm_name( with pytest.raises(Exception) as resource_not_found: create_state_machine( - aws_client, name=sm_name, definition=definition_str_2, roleArn=snf_role_arn + aws_client_no_retry, name=sm_name, definition=definition_str_2, roleArn=snf_role_arn ) sfn_snapshot.match("already_exists_1", resource_not_found.value.response) @@ -307,7 +331,8 @@ def test_list_sms( state_machine_arns.append(state_machine_arn) await_state_machine_listed( - stepfunctions_client=aws_client.stepfunctions, state_machine_arn=state_machine_arn + stepfunctions_client=aws_client.stepfunctions, + state_machine_arn=state_machine_arn, ) lst_resp = aws_client.stepfunctions.list_state_machines() @@ -321,7 +346,8 @@ def test_list_sms( sfn_snapshot.match(f"deletion_resp_{i}", deletion_resp) await_state_machine_not_listed( - stepfunctions_client=aws_client.stepfunctions, state_machine_arn=state_machine_arn + stepfunctions_client=aws_client.stepfunctions, + state_machine_arn=state_machine_arn, ) lst_resp = aws_client.stepfunctions.list_state_machines() @@ -330,7 +356,12 @@ def test_list_sms( @markers.aws.validated def test_list_sms_pagination( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) @@ -393,12 +424,12 @@ def _verify_paginate_results() -> list: # maxResults value is out of bounds with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machines(maxResults=1001) + aws_client_no_retry.stepfunctions.list_state_machines(maxResults=1001) sfn_snapshot.match("list-state-machines-invalid-param-too-large", err.value.response) # nextToken is too short with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machines(nextToken="") + aws_client_no_retry.stepfunctions.list_state_machines(nextToken="") sfn_snapshot.match( "list-state-machines-invalid-param-short-nextToken", {"exception_typename": err.typename, "exception_value": err.value}, @@ -407,7 +438,7 @@ def _verify_paginate_results() -> list: # nextToken is too long invalid_long_token = "x" * 1025 with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machines(nextToken=invalid_long_token) + aws_client_no_retry.stepfunctions.list_state_machines(nextToken=invalid_long_token) sfn_snapshot.add_transformer( RegexTransformer(invalid_long_token, f"") ) @@ -435,6 +466,7 @@ def test_start_execution_idempotent( sqs_create_queue, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( @@ -488,7 +520,7 @@ def test_start_execution_idempotent( # Should fail because the execution has the same 'name' as another but a different 'input'. with pytest.raises(Exception) as err: - aws_client.stepfunctions.start_execution( + aws_client_no_retry.stepfunctions.start_execution( stateMachineArn=state_machine_arn, input='{"body" : "different-data"}', name=execution_name, @@ -542,7 +574,12 @@ def test_start_execution( @markers.aws.validated def test_list_execution_no_such_state_machine( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -561,16 +598,18 @@ def test_list_execution_no_such_state_machine( sfn_snapshot.add_transformer(RegexTransformer(sm_nonexistent_arn, "ssm_nonexistent_arn")) with pytest.raises(Exception) as exc: - aws_client.stepfunctions.list_executions(stateMachineArn=sm_nonexistent_arn) + aws_client_no_retry.stepfunctions.list_executions(stateMachineArn=sm_nonexistent_arn) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) - def test_list_execution_invalid_arn(self, sfn_snapshot, aws_client): + def test_list_execution_invalid_arn(self, sfn_snapshot, aws_client, aws_client_no_retry): with pytest.raises(Exception) as exc: - aws_client.stepfunctions.list_executions(stateMachineArn="invalid_state_machine_arn") + aws_client_no_retry.stepfunctions.list_executions( + stateMachineArn="invalid_state_machine_arn" + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @@ -578,7 +617,12 @@ def test_list_execution_invalid_arn(self, sfn_snapshot, aws_client): @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value", "$..redriveCount"]) def test_list_executions_pagination( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) @@ -598,7 +642,8 @@ def test_list_executions_pagination( state_machine_arn = creation_resp["stateMachineArn"] await_state_machine_listed( - stepfunctions_client=aws_client.stepfunctions, state_machine_arn=state_machine_arn + stepfunctions_client=aws_client.stepfunctions, + state_machine_arn=state_machine_arn, ) execution_arns = list() @@ -638,14 +683,14 @@ def test_list_executions_pagination( # maxResults value is out of bounds with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_arn, maxResults=1001 ) sfn_snapshot.match("list-executions-invalid-param-too-large", err.value.response) # nextToken is too short with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_arn, nextToken="" ) sfn_snapshot.match( @@ -656,7 +701,7 @@ def test_list_executions_pagination( # nextToken is too long invalid_long_token = "x" * 3097 with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_arn, nextToken=invalid_long_token ) sfn_snapshot.add_transformer( @@ -679,7 +724,12 @@ def test_list_executions_pagination( @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value", "$..redriveCount"]) def test_list_executions_versions_pagination( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) @@ -691,7 +741,11 @@ def test_list_executions_versions_pagination( sm_name = f"statemachine_{short_uid()}" creation_resp = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp, 0)) @@ -742,14 +796,14 @@ def test_list_executions_versions_pagination( # maxResults value is out of bounds with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_version_arn, maxResults=1001 ) sfn_snapshot.match("list-executions-invalid-param-too-large", err.value.response) # nextToken is too short with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_version_arn, nextToken="" ) sfn_snapshot.match( @@ -760,7 +814,7 @@ def test_list_executions_versions_pagination( # nextToken is too long invalid_long_token = "x" * 3097 with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_version_arn, nextToken=invalid_long_token ) sfn_snapshot.add_transformer( @@ -812,7 +866,12 @@ def test_get_execution_history_reversed( @markers.aws.validated def test_invalid_start_execution_arn( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -834,13 +893,20 @@ def test_invalid_start_execution_arn( aws_client.stepfunctions.delete_state_machine(stateMachineArn=state_machine_arn) with pytest.raises(Exception) as resource_not_found: - aws_client.stepfunctions.start_execution(stateMachineArn=state_machine_arn_invalid) + aws_client_no_retry.stepfunctions.start_execution( + stateMachineArn=state_machine_arn_invalid + ) sfn_snapshot.match("start_exec_of_deleted", resource_not_found.value.response) @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message", "$..message"]) @markers.aws.validated def test_invalid_start_execution_input( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -857,19 +923,21 @@ def test_invalid_start_execution_input( state_machine_arn = creation_resp["stateMachineArn"] with pytest.raises(Exception) as err: - aws_client.stepfunctions.start_execution( + aws_client_no_retry.stepfunctions.start_execution( stateMachineArn=state_machine_arn, input="not some json" ) sfn_snapshot.match("start_exec_str_inp", err.value.response) with pytest.raises(Exception) as err: - aws_client.stepfunctions.start_execution( + aws_client_no_retry.stepfunctions.start_execution( stateMachineArn=state_machine_arn, input="{'not': 'json'" ) sfn_snapshot.match("start_exec_not_json_inp", err.value.response) with pytest.raises(Exception) as err: - aws_client.stepfunctions.start_execution(stateMachineArn=state_machine_arn, input="") + aws_client_no_retry.stepfunctions.start_execution( + stateMachineArn=state_machine_arn, input="" + ) sfn_snapshot.match("start_res_empty", err.value.response) start_res_num = aws_client.stepfunctions.start_execution( @@ -1097,7 +1165,12 @@ def test_create_update_state_machine_base_definition_and_role( @markers.aws.validated def test_create_update_state_machine_base_update_none( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -1119,11 +1192,13 @@ def test_create_update_state_machine_base_update_none( sfn_snapshot.match("describe_resp_t0", describe_resp_t0) with pytest.raises(Exception) as missing_required_parameter: - aws_client.stepfunctions.update_state_machine(stateMachineArn=state_machine_arn) + aws_client_no_retry.stepfunctions.update_state_machine( + stateMachineArn=state_machine_arn + ) sfn_snapshot.match("missing_required_parameter", missing_required_parameter.value.response) with pytest.raises(Exception) as null_required_parameter: - aws_client.stepfunctions.update_state_machine( + aws_client_no_retry.stepfunctions.update_state_machine( stateMachineArn=state_machine_arn, definition=None, roleArn=None ) sfn_snapshot.match("null_required_parameter", null_required_parameter.value) @@ -1358,7 +1433,12 @@ def test_describe_execution( @markers.aws.validated def test_describe_execution_no_such_state_machine( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -1386,16 +1466,18 @@ def test_describe_execution_no_such_state_machine( ) with pytest.raises(Exception) as exc: - aws_client.stepfunctions.describe_execution(executionArn=invalid_execution_arn) + aws_client_no_retry.stepfunctions.describe_execution(executionArn=invalid_execution_arn) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) - def test_describe_execution_invalid_arn(self, sfn_snapshot, aws_client): + def test_describe_execution_invalid_arn(self, sfn_snapshot, aws_client_no_retry): with pytest.raises(Exception) as exc: - aws_client.stepfunctions.describe_execution(executionArn="invalid_state_machine_arn") + aws_client_no_retry.stepfunctions.describe_execution( + executionArn="invalid_state_machine_arn" + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @@ -1438,7 +1520,12 @@ def test_describe_execution_arn_containing_punctuation( @markers.aws.needs_fixing def test_get_execution_history_no_such_execution( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -1461,16 +1548,20 @@ def test_get_execution_history_no_such_execution( ) with pytest.raises(Exception) as exc: - aws_client.stepfunctions.get_execution_history(executionArn=invalid_execution_arn) + aws_client_no_retry.stepfunctions.get_execution_history( + executionArn=invalid_execution_arn + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) - def test_get_execution_history_invalid_arn(self, sfn_snapshot, aws_client): + def test_get_execution_history_invalid_arn(self, sfn_snapshot, aws_client_no_retry): with pytest.raises(Exception) as exc: - aws_client.stepfunctions.get_execution_history(executionArn="invalid_state_machine_arn") + aws_client_no_retry.stepfunctions.get_execution_history( + executionArn="invalid_state_machine_arn" + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @@ -1478,7 +1569,12 @@ def test_get_execution_history_invalid_arn(self, sfn_snapshot, aws_client): @markers.snapshot.skip_snapshot_verify(paths=["$..redriveCount"]) @markers.aws.validated def test_state_machine_status_filter( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -1522,7 +1618,7 @@ def test_state_machine_status_filter( sfn_snapshot.match("list_running_when_complete", list_response) with pytest.raises(ClientError) as e: - aws_client.stepfunctions.list_executions( + aws_client_no_retry.stepfunctions.list_executions( stateMachineArn=state_machine_arn, statusFilter="succeeded" ) sfn_snapshot.match("list_executions_filter_exc", e.value.response) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py index 34c31e7c8cc33..8dcee8bceeac7 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py @@ -95,18 +95,15 @@ def test_create_describe_delete_activity( ], ) def test_create_activity_invalid_name( - self, create_activity, sfn_snapshot, aws_client, activity_name + self, create_activity, sfn_snapshot, aws_client_no_retry, activity_name ): with pytest.raises(ClientError) as e: - aws_client.stepfunctions.create_activity(name=activity_name) + aws_client_no_retry.stepfunctions.create_activity(name=activity_name) sfn_snapshot.match("invalid_name", e.value.response) @markers.aws.validated def test_describe_deleted_activity( - self, - create_activity, - sfn_snapshot, - aws_client, + self, create_activity, sfn_snapshot, aws_client, aws_client_no_retry ): create_activity_response = aws_client.stepfunctions.create_activity( name=f"TestActivity-{short_uid()}" @@ -115,7 +112,7 @@ def test_describe_deleted_activity( sfn_snapshot.add_transformer(RegexTransformer(activity_arn, "activity_arn")) aws_client.stepfunctions.delete_activity(activityArn=activity_arn) with pytest.raises(ClientError) as e: - aws_client.stepfunctions.describe_activity(activityArn=activity_arn) + aws_client_no_retry.stepfunctions.describe_activity(activityArn=activity_arn) sfn_snapshot.match("no_such_activity", e.value.response) @markers.aws.validated @@ -123,20 +120,17 @@ def test_describe_deleted_activity( def test_describe_activity_invalid_arn( self, sfn_snapshot, - aws_client, + aws_client_no_retry, ): with pytest.raises(ClientError) as exc: - aws_client.stepfunctions.describe_activity(activityArn="no_an_activity_arn") + aws_client_no_retry.stepfunctions.describe_activity(activityArn="no_an_activity_arn") sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @markers.aws.validated def test_get_activity_task_deleted( - self, - create_activity, - sfn_snapshot, - aws_client, + self, create_activity, sfn_snapshot, aws_client, aws_client_no_retry ): create_activity_response = aws_client.stepfunctions.create_activity( name=f"TestActivity-{short_uid()}" @@ -145,7 +139,7 @@ def test_get_activity_task_deleted( sfn_snapshot.add_transformer(RegexTransformer(activity_arn, "activity_arn")) aws_client.stepfunctions.delete_activity(activityArn=activity_arn) with pytest.raises(ClientError) as e: - aws_client.stepfunctions.get_activity_task(activityArn=activity_arn) + aws_client_no_retry.stepfunctions.get_activity_task(activityArn=activity_arn) sfn_snapshot.match("no_such_activity", e.value.response) @markers.aws.validated @@ -153,10 +147,10 @@ def test_get_activity_task_deleted( def test_get_activity_task_invalid_arn( self, sfn_snapshot, - aws_client, + aws_client_no_retry, ): with pytest.raises(ClientError) as exc: - aws_client.stepfunctions.get_activity_task(activityArn="no_an_activity_arn") + aws_client_no_retry.stepfunctions.get_activity_task(activityArn="no_an_activity_arn") sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py index eb5527c821e7a..abc1d2aff87e7 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py @@ -80,6 +80,7 @@ def test_error_create_alias_with_state_machine_arn( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(sfn_role_arn, "sfn_role_arn")) @@ -102,7 +103,7 @@ def test_error_create_alias_with_state_machine_arn( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, description="create state machine alias description", name=f"AliasName-{short_uid()}", routingConfiguration=[ @@ -123,6 +124,7 @@ def test_error_create_alias_not_idempotent( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(sfn_role_arn, "sfn_role_arn")) @@ -171,7 +173,7 @@ def test_error_create_alias_not_idempotent( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, description="This is a different description", name=state_machine_alias_name, routingConfiguration=state_machine_alias_routing_configuration, @@ -183,7 +185,7 @@ def test_error_create_alias_not_idempotent( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, description=state_machine_alias_description, name=state_machine_alias_name, routingConfiguration=[ @@ -280,6 +282,7 @@ def test_error_create_alias_invalid_router_configs( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_client = aws_client.stepfunctions @@ -312,7 +315,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[], ) @@ -322,7 +325,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -342,7 +345,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -359,7 +362,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -376,7 +379,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -390,7 +393,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -404,7 +407,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -422,7 +425,7 @@ def test_error_create_alias_invalid_router_configs( with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, name=f"AliasName-{short_uid()}", routingConfiguration=[ RoutingConfigurationListItem( @@ -446,6 +449,7 @@ def test_error_create_alias_invalid_name( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(sfn_role_arn, "sfn_role_arn")) @@ -470,7 +474,7 @@ def test_error_create_alias_invalid_name( for invalid_name in invalid_names: with pytest.raises(Exception) as exc: create_state_machine_alias( - target_aws_client=aws_client, + target_aws_client=aws_client_no_retry, description="create state machine alias description", name=invalid_name, routingConfiguration=[ @@ -592,6 +596,7 @@ def test_update_no_such_alias_arn( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_client = aws_client.stepfunctions @@ -647,7 +652,7 @@ def test_update_no_such_alias_arn( ) with pytest.raises(Exception) as exc: - sfn_client.update_state_machine_alias( + aws_client_no_retry.stepfunctions.update_state_machine_alias( stateMachineAliasArn=state_machine_alias_arn, description="Updated state machine alias description", ) @@ -863,6 +868,7 @@ def test_delete_version_with_alias( create_state_machine_alias, sfn_snapshot, aws_client, + aws_client_no_retry, ): sfn_client = aws_client.stepfunctions @@ -911,7 +917,7 @@ def test_delete_version_with_alias( ) with pytest.raises(Exception) as exc: - sfn_client.delete_state_machine_version( + aws_client_no_retry.stepfunctions.delete_state_machine_version( stateMachineVersionArn=state_machine_version_arn ) sfn_snapshot.match( diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_express.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_express.py index 9db90757eff92..c193ee4432cb7 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_express.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_express.py @@ -73,6 +73,7 @@ def test_start_async_describe_history_execution( sfn_create_log_group, sfn_snapshot, aws_client, + aws_client_no_retry, ): definition = ServicesTemplates.load_sfn_template(BaseTemplate.BASE_PASS_RESULT) definition_str = json.dumps(definition) @@ -88,19 +89,19 @@ def test_start_async_describe_history_execution( ) with pytest.raises(Exception) as ex: - aws_client.stepfunctions.list_executions(stateMachineArn=state_machine_arn) + aws_client_no_retry.stepfunctions.list_executions(stateMachineArn=state_machine_arn) sfn_snapshot.match("list_executions_error", ex.value.response) with pytest.raises(Exception) as ex: - aws_client.stepfunctions.describe_execution(executionArn=execution_arn) + aws_client_no_retry.stepfunctions.describe_execution(executionArn=execution_arn) sfn_snapshot.match("describe_execution_error", ex.value.response) with pytest.raises(Exception) as ex: - aws_client.stepfunctions.stop_execution(executionArn=execution_arn) + aws_client_no_retry.stepfunctions.stop_execution(executionArn=execution_arn) sfn_snapshot.match("stop_execution_error", ex.value.response) with pytest.raises(Exception) as ex: - aws_client.stepfunctions.get_execution_history(executionArn=execution_arn) + aws_client_no_retry.stepfunctions.get_execution_history(executionArn=execution_arn) sfn_snapshot.match("get_execution_history_error", ex.value.response) @markers.aws.validated @@ -137,6 +138,7 @@ def test_start_sync_execution( def test_illegal_callbacks( self, aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -150,7 +152,7 @@ def test_illegal_callbacks( with pytest.raises(Exception) as ex: create_state_machine( - aws_client, + aws_client_no_retry, name=f"express_statemachine_{short_uid()}", definition=definition, roleArn=snf_role_arn, @@ -163,6 +165,7 @@ def test_illegal_callbacks( def test_illegal_activity_task( self, aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, create_activity, @@ -185,7 +188,7 @@ def test_illegal_activity_task( with pytest.raises(Exception) as ex: create_state_machine( - aws_client, + aws_client_no_retry, name=f"express_statemachine_{short_uid()}", definition=definition, roleArn=snf_role_arn, diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_logs.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_logs.py index b8838b85632fa..75d6af6532d87 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_logs.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_logs.py @@ -131,7 +131,7 @@ def test_invalid_logging_configuration( sfn_create_log_group, sfn_snapshot, aws_client, - aws_client_factory, + aws_client_no_retry, logging_configuration, ): snf_role_arn = create_state_machine_iam_role(aws_client) @@ -142,11 +142,8 @@ def test_invalid_logging_configuration( sm_name = f"statemachine_{short_uid()}" - stepfunctions_client = aws_client_factory( - config=Config(parameter_validation=False) - ).stepfunctions with pytest.raises(ClientError) as exc: - stepfunctions_client.create_state_machine( + aws_client_no_retry.stepfunctions.create_state_machine( name=sm_name, definition=definition, roleArn=snf_role_arn, @@ -164,6 +161,7 @@ def test_deleted_log_group( sfn_create_log_group, sfn_snapshot, aws_client, + aws_client_no_retry, ): logs_client = aws_client.logs log_group_name = sfn_create_log_group() @@ -195,7 +193,7 @@ def _log_group_is_deleted() -> bool: with pytest.raises(ClientError) as exc: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -214,6 +212,7 @@ def test_multiple_destinations( sfn_create_log_group, sfn_snapshot, aws_client, + aws_client_no_retry, ): logging_configuration = LoggingConfiguration(level=LogLevel.ALL, destinations=[]) for i in range(2): @@ -232,7 +231,7 @@ def test_multiple_destinations( with pytest.raises(ClientError) as exc: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -252,6 +251,7 @@ def test_update_logging_configuration( sfn_snapshot, aws_client, aws_client_factory, + aws_client_no_retry, ): stepfunctions_client = aws_client_factory( config=Config(parameter_validation=False) @@ -334,7 +334,7 @@ def test_update_logging_configuration( ) ) with pytest.raises(ClientError) as exc: - stepfunctions_client.update_state_machine( + aws_client_no_retry.stepfunctions.update_state_machine( stateMachineArn=state_machine_arn, loggingConfiguration=base_logging_configuration ) sfn_snapshot.match( diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py index 33a388c258f46..c68a5c5d9828d 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py @@ -86,7 +86,7 @@ def test_list_map_runs_and_describe_map_run( ) def test_map_state_label_invalid_char_fail( self, - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -98,7 +98,7 @@ def test_map_state_label_invalid_char_fail( with pytest.raises(Exception) as err: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -108,14 +108,14 @@ def test_map_state_label_invalid_char_fail( @markers.aws.validated def test_map_state_label_empty_fail( - self, aws_client, create_state_machine_iam_role, create_state_machine, sfn_snapshot + self, aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot ): template = ST.load_sfn_template(ST.MAP_STATE_LABEL_EMPTY_FAIL) definition = json.dumps(template) with pytest.raises(Exception) as err: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, @@ -125,14 +125,14 @@ def test_map_state_label_empty_fail( @markers.aws.validated def test_map_state_label_too_long_fail( - self, aws_client, create_state_machine_iam_role, create_state_machine, sfn_snapshot + self, aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot ): template = ST.load_sfn_template(ST.MAP_STATE_LABEL_TOO_LONG_FAIL) definition = json.dumps(template) with pytest.raises(Exception) as err: create_state_machine_with_iam_role( - aws_client, + aws_client_no_retry, create_state_machine_iam_role, create_state_machine, sfn_snapshot, diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py index 5a7a80956d3d9..a08fc22a8691a 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py @@ -69,6 +69,7 @@ def test_tag_invalid_state_machine( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, tag_list, ): snf_role_arn = create_state_machine_iam_role(aws_client) @@ -86,7 +87,9 @@ def test_tag_invalid_state_machine( sfn_snapshot.match("creation_resp_1", creation_resp_1) with pytest.raises(Exception) as error: - aws_client.stepfunctions.tag_resource(resourceArn=state_machine_arn, tags=tag_list) + aws_client_no_retry.stepfunctions.tag_resource( + resourceArn=state_machine_arn, tags=tag_list + ) sfn_snapshot.match("error", error.value) @markers.aws.validated @@ -96,6 +99,7 @@ def test_tag_state_machine_version( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -118,7 +122,7 @@ def test_tag_state_machine_version( sfn_snapshot.match("publish_resp", publish_resp) with pytest.raises(Exception) as error: - aws_client.stepfunctions.tag_resource( + aws_client_no_retry.stepfunctions.tag_resource( resourceArn=state_machine_version_arn, tags=[Tag(key="key1", value="value1")] ) sfn_snapshot.match("error", error.value) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_variable_references.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_variable_references.py index d9b87eba5151d..9931487a2a077 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_variable_references.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_variable_references.py @@ -91,7 +91,10 @@ def test_base_variable_references_in_assign_templates( definition_str = json.dumps(definition) creation_response = create_state_machine( - aws_client, name=f"sm-{short_uid()}", definition=definition_str, roleArn=snf_role_arn + aws_client, + name=f"sm-{short_uid()}", + definition=definition_str, + roleArn=snf_role_arn, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_response, 0)) state_machine_arn = creation_response["stateMachineArn"] @@ -146,7 +149,10 @@ def test_base_variable_references_in_jsonata_template( definition_str = json.dumps(definition) creation_response = create_state_machine( - aws_client, name=f"sm-{short_uid()}", definition=definition_str, roleArn=snf_role_arn + aws_client, + name=f"sm-{short_uid()}", + definition=definition_str, + roleArn=snf_role_arn, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_response, 0)) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py index bbb838fadf465..2a9e7dd020e8e 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py @@ -33,7 +33,11 @@ def test_create_with_publish( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -71,6 +75,7 @@ def test_create_with_version_description_no_publish( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -81,7 +86,7 @@ def test_create_with_version_description_no_publish( with pytest.raises(Exception) as validation_exception: sm_name = f"statemachine_{short_uid()}" create_state_machine( - aws_client, + aws_client_no_retry, name=sm_name, definition=definition_str, roleArn=snf_role_arn, @@ -105,7 +110,11 @@ def test_create_publish_describe_no_version_description( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -167,6 +176,7 @@ def test_list_state_machine_versions_pagination( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -197,7 +207,9 @@ def test_list_state_machine_versions_pagination( state_machine_version_arns.append(state_machine_version_arn) await_state_machine_version_listed( - aws_client.stepfunctions, state_machine_arn, update_resp_1["stateMachineVersionArn"] + aws_client.stepfunctions, + state_machine_arn, + update_resp_1["stateMachineVersionArn"], ) page_1_state_machine_versions = aws_client.stepfunctions.list_state_machine_versions( @@ -221,7 +233,7 @@ def test_list_state_machine_versions_pagination( # maxResults value is out of bounds with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machine_versions( + aws_client_no_retry.stepfunctions.list_state_machine_versions( stateMachineArn=state_machine_arn, maxResults=1001 ) sfn_snapshot.match( @@ -230,7 +242,7 @@ def test_list_state_machine_versions_pagination( # nextToken is too short with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machine_versions( + aws_client_no_retry.stepfunctions.list_state_machine_versions( stateMachineArn=state_machine_arn, nextToken="" ) sfn_snapshot.match( @@ -241,7 +253,7 @@ def test_list_state_machine_versions_pagination( # nextToken is too long invalid_long_token = "x" * 1025 with pytest.raises(Exception) as err: - aws_client.stepfunctions.list_state_machine_versions( + aws_client_no_retry.stepfunctions.list_state_machine_versions( stateMachineArn=state_machine_arn, nextToken=invalid_long_token ) sfn_snapshot.add_transformer( @@ -292,7 +304,11 @@ def test_list_delete_version( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -339,6 +355,7 @@ def test_update_state_machine( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -364,7 +381,9 @@ def test_update_state_machine( sfn_snapshot.match("update_resp_1", update_resp_1) await_state_machine_version_listed( - aws_client.stepfunctions, state_machine_arn, update_resp_1["stateMachineVersionArn"] + aws_client.stepfunctions, + state_machine_arn, + update_resp_1["stateMachineVersionArn"], ) list_versions_resp_1 = aws_client.stepfunctions.list_state_machine_versions( @@ -383,7 +402,9 @@ def test_update_state_machine( state_machine_version_2_arn = update_resp_2["stateMachineVersionArn"] await_state_machine_version_listed( - aws_client.stepfunctions, state_machine_arn, update_resp_2["stateMachineVersionArn"] + aws_client.stepfunctions, + state_machine_arn, + update_resp_2["stateMachineVersionArn"], ) list_versions_resp_2 = aws_client.stepfunctions.list_state_machine_versions( @@ -396,13 +417,13 @@ def test_update_state_machine( definition_r3_str = json.dumps(definition_r3) with pytest.raises(Exception) as invalid_arn_1: - aws_client.stepfunctions.update_state_machine( + aws_client_no_retry.stepfunctions.update_state_machine( stateMachineArn=state_machine_version_2_arn, definition=definition_r3_str ) sfn_snapshot.match("invalid_arn_1", invalid_arn_1.value.response) with pytest.raises(Exception) as invalid_arn_2: - aws_client.stepfunctions.update_state_machine( + aws_client_no_retry.stepfunctions.update_state_machine( stateMachineArn=state_machine_version_2_arn, definition=definition_r3_str, publish=True, @@ -416,6 +437,7 @@ def test_publish_state_machine_version( create_state_machine, sfn_snapshot, aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -500,7 +522,7 @@ def test_publish_state_machine_version( sfn_snapshot.match("update_resp_3", update_resp_3) with pytest.raises(Exception) as conflict_exception: - aws_client.stepfunctions.publish_state_machine_version( + aws_client_no_retry.stepfunctions.publish_state_machine_version( stateMachineArn=state_machine_arn, revisionId=revision_id_r2 ) sfn_snapshot.match("conflict_exception", conflict_exception.value) @@ -521,7 +543,11 @@ def test_start_version_execution( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -556,7 +582,8 @@ def test_start_version_execution( version_execution_arn = execution_version_resp["executionArn"] await_execution_terminated( - stepfunctions_client=aws_client.stepfunctions, execution_arn=version_execution_arn + stepfunctions_client=aws_client.stepfunctions, + execution_arn=version_execution_arn, ) await_execution_lists_terminated( @@ -586,7 +613,11 @@ def test_version_ids_between_deletions( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -661,7 +692,12 @@ def test_idempotent_publish( @markers.aws.validated def test_publish_state_machine_version_no_such_machine( - self, create_state_machine_iam_role, create_state_machine, sfn_snapshot, aws_client + self, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + aws_client, + aws_client_no_retry, ): snf_role_arn = create_state_machine_iam_role(aws_client) sfn_snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -680,7 +716,7 @@ def test_publish_state_machine_version_no_such_machine( sfn_snapshot.add_transformer(RegexTransformer(sm_nonexistent_arn, "ssm_nonexistent_arn")) with pytest.raises(Exception) as exc: - aws_client.stepfunctions.publish_state_machine_version( + aws_client_no_retry.stepfunctions.publish_state_machine_version( stateMachineArn=sm_nonexistent_arn ) sfn_snapshot.match( @@ -689,9 +725,11 @@ def test_publish_state_machine_version_no_such_machine( @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..exception_value"]) - def test_publish_state_machine_version_invalid_arn(self, sfn_snapshot, aws_client): + def test_publish_state_machine_version_invalid_arn(self, sfn_snapshot, aws_client_no_retry): with pytest.raises(Exception) as exc: - aws_client.stepfunctions.publish_state_machine_version(stateMachineArn="invalid_arn") + aws_client_no_retry.stepfunctions.publish_state_machine_version( + stateMachineArn="invalid_arn" + ) sfn_snapshot.match( "exception", {"exception_typename": exc.typename, "exception_value": exc.value} ) @@ -712,7 +750,11 @@ def test_empty_revision_with_publish_and_publish_on_creation( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) @@ -776,7 +818,11 @@ def test_describe_state_machine_for_execution_of_version( sm_name = f"statemachine_{short_uid()}" creation_resp_1 = create_state_machine( - aws_client, name=sm_name, definition=definition_str, roleArn=snf_role_arn, publish=True + aws_client, + name=sm_name, + definition=definition_str, + roleArn=snf_role_arn, + publish=True, ) sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_resp_1, 0)) sfn_snapshot.match("creation_resp_1", creation_resp_1) From bb68b2e005b98fee797383d709f627e67a247329 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Thu, 3 Apr 2025 13:54:36 +0200 Subject: [PATCH 020/108] Reapply reduce requests necessary for log publishing from lambda to cloudwatch logs (#12470) --- .../services/lambda_/invocation/logs.py | 32 ++++++++-- .../functions/lambda_cloudwatch_logs.py | 10 +++ tests/aws/services/lambda_/test_lambda.py | 1 + .../services/lambda_/test_lambda_runtimes.py | 60 ++++++++++++++++++ .../test_lambda_runtimes.snapshot.json | 63 +++++++++++++++++++ .../test_lambda_runtimes.validation.json | 3 + 6 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 tests/aws/services/lambda_/functions/lambda_cloudwatch_logs.py diff --git a/localstack-core/localstack/services/lambda_/invocation/logs.py b/localstack-core/localstack/services/lambda_/invocation/logs.py index a63f1ab2d04f4..2ff2ab35d951b 100644 --- a/localstack-core/localstack/services/lambda_/invocation/logs.py +++ b/localstack-core/localstack/services/lambda_/invocation/logs.py @@ -1,13 +1,13 @@ import dataclasses import logging import threading +import time from queue import Queue from typing import Optional, Union from localstack.aws.connect import connect_to from localstack.utils.aws.client_types import ServicePrincipal from localstack.utils.bootstrap import is_api_enabled -from localstack.utils.cloudwatch.cloudwatch_util import store_cloudwatch_logs from localstack.utils.threads import FuncThread LOG = logging.getLogger(__name__) @@ -50,10 +50,34 @@ def run_log_loop(self, *args, **kwargs) -> None: log_item = self.log_queue.get() if log_item is QUEUE_SHUTDOWN: return + # we need to split by newline - but keep the newlines in the strings + # strips empty lines, as they are not accepted by cloudwatch + logs = [line + "\n" for line in log_item.logs.split("\n") if line] + # until we have a better way to have timestamps, log events have the same time for a single invocation + log_events = [ + {"timestamp": int(time.time() * 1000), "message": log_line} for log_line in logs + ] try: - store_cloudwatch_logs( - logs_client, log_item.log_group, log_item.log_stream, log_item.logs - ) + try: + logs_client.put_log_events( + logGroupName=log_item.log_group, + logStreamName=log_item.log_stream, + logEvents=log_events, + ) + except logs_client.exceptions.ResourceNotFoundException: + # create new log group + try: + logs_client.create_log_group(logGroupName=log_item.log_group) + except logs_client.exceptions.ResourceAlreadyExistsException: + pass + logs_client.create_log_stream( + logGroupName=log_item.log_group, logStreamName=log_item.log_stream + ) + logs_client.put_log_events( + logGroupName=log_item.log_group, + logStreamName=log_item.log_stream, + logEvents=log_events, + ) except Exception as e: LOG.warning( "Error saving logs to group %s in region %s: %s", diff --git a/tests/aws/services/lambda_/functions/lambda_cloudwatch_logs.py b/tests/aws/services/lambda_/functions/lambda_cloudwatch_logs.py new file mode 100644 index 0000000000000..354749aa06122 --- /dev/null +++ b/tests/aws/services/lambda_/functions/lambda_cloudwatch_logs.py @@ -0,0 +1,10 @@ +""" +A simple handler which does a print on the "body" key of the event passed in. +Can be used to log different payloads, to check for the correct format in cloudwatch logs +""" + + +def handler(event, context): + # Just print the log line that was passed to lambda + print(event["body"]) + return event diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index ad50d73d258e7..700361d32ebbb 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -127,6 +127,7 @@ THIS_FOLDER, "functions/lambda_multiple_handlers.py" ) TEST_LAMBDA_NOTIFIER = os.path.join(THIS_FOLDER, "functions/lambda_notifier.py") +TEST_LAMBDA_CLOUDWATCH_LOGS = os.path.join(THIS_FOLDER, "functions/lambda_cloudwatch_logs.py") PYTHON_TEST_RUNTIMES = RUNTIMES_AGGREGATED["python"] NODE_TEST_RUNTIMES = RUNTIMES_AGGREGATED["nodejs"] diff --git a/tests/aws/services/lambda_/test_lambda_runtimes.py b/tests/aws/services/lambda_/test_lambda_runtimes.py index 6542229ce6e61..6c0e82bbec038 100644 --- a/tests/aws/services/lambda_/test_lambda_runtimes.py +++ b/tests/aws/services/lambda_/test_lambda_runtimes.py @@ -6,6 +6,7 @@ import json import os import shutil +import textwrap from typing import List import pytest @@ -26,6 +27,7 @@ JAVA_TEST_RUNTIMES, NODE_TEST_RUNTIMES, PYTHON_TEST_RUNTIMES, + TEST_LAMBDA_CLOUDWATCH_LOGS, TEST_LAMBDA_JAVA_MULTIPLE_HANDLERS, TEST_LAMBDA_JAVA_WITH_LIB, TEST_LAMBDA_NODEJS_ES6, @@ -484,3 +486,61 @@ def test_manual_endpoint_injection(self, multiruntime_lambda, tmp_path, aws_clie FunctionName=create_function_result["FunctionName"], ) assert "FunctionError" not in invocation_result + + +class TestCloudwatchLogs: + @pytest.fixture(autouse=True) + def snapshot_transformers(self, snapshot): + snapshot.add_transformer(snapshot.transform.lambda_report_logs()) + snapshot.add_transformer( + snapshot.transform.key_value("eventId", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.regex(r"::runtime:\w+", "::runtime:") + ) + snapshot.add_transformer(snapshot.transform.regex("\\.v\\d{2}", ".v")) + + @markers.aws.validated + # skip all snapshots - the logs are too different + # TODO add INIT_START to make snapshotting of logs possible + @markers.snapshot.skip_snapshot_verify() + def test_multi_line_prints(self, aws_client, create_lambda_function, snapshot): + function_name = f"test_lambda_{short_uid()}" + log_group_name = f"/aws/lambda/{function_name}" + create_lambda_function( + func_name=function_name, + handler_file=TEST_LAMBDA_CLOUDWATCH_LOGS, + runtime=Runtime.python3_13, + ) + + payload = { + "body": textwrap.dedent(""" + multi + line + string + another\rline + """) + } + invoke_response = aws_client.lambda_.invoke( + FunctionName=function_name, Payload=json.dumps(payload) + ) + snapshot.add_transformer( + snapshot.transform.regex( + invoke_response["ResponseMetadata"]["RequestId"], "" + ) + ) + + def fetch_logs(): + log_events_result = aws_client.logs.filter_log_events(logGroupName=log_group_name) + assert any("REPORT" in e["message"] for e in log_events_result["events"]) + return log_events_result["events"] + + log_events = retry(fetch_logs, retries=10, sleep=2) + snapshot.match("log-events", log_events) + + log_messages = [log["message"] for log in log_events] + # some manual assertions until we can actually use the snapshot + assert "multi\n" in log_messages + assert "line\n" in log_messages + assert "string\n" in log_messages + assert "another\rline\n" in log_messages diff --git a/tests/aws/services/lambda_/test_lambda_runtimes.snapshot.json b/tests/aws/services/lambda_/test_lambda_runtimes.snapshot.json index 248a31729f39f..314aec2afb7e4 100644 --- a/tests/aws/services/lambda_/test_lambda_runtimes.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_runtimes.snapshot.json @@ -1208,5 +1208,68 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestCloudwatchLogs::test_multi_line_prints": { + "recorded-date": "02-04-2025, 12:35:33", + "recorded-content": { + "log-events": [ + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "INIT_START Runtime Version: python:3.13.v\tRuntime Version ARN: arn::lambda:::runtime:\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "START RequestId: Version: $LATEST\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "multi\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "line\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "string\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "another\rline\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "END RequestId: \n", + "ingestionTime": "timestamp", + "eventId": "event-id" + }, + { + "logStreamName": "", + "timestamp": "timestamp", + "message": "REPORT RequestId: \tDuration: ms\tBilled Duration: ms\tMemory Size: 128 MB\tMax Memory Used: MB\tInit Duration: ms\t\n", + "ingestionTime": "timestamp", + "eventId": "event-id" + } + ] + } } } diff --git a/tests/aws/services/lambda_/test_lambda_runtimes.validation.json b/tests/aws/services/lambda_/test_lambda_runtimes.validation.json index 0b47eb2edb90c..4d29b8b622534 100644 --- a/tests/aws/services/lambda_/test_lambda_runtimes.validation.json +++ b/tests/aws/services/lambda_/test_lambda_runtimes.validation.json @@ -1,4 +1,7 @@ { + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestCloudwatchLogs::test_multi_line_prints": { + "last_validated_date": "2025-04-02T12:35:33+00:00" + }, "tests/aws/services/lambda_/test_lambda_runtimes.py::TestGoProvidedRuntimes::test_manual_endpoint_injection[provided.al2023]": { "last_validated_date": "2024-11-26T09:46:59+00:00" }, From 81772d2ed6979a872df11c25aa2c5e3c9039a140 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:31:33 +0200 Subject: [PATCH 021/108] add VerifiedPermissions to the client types (#12474) --- localstack-core/localstack/utils/aws/client_types.py | 4 ++++ pyproject.toml | 2 +- requirements-typehint.txt | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/utils/aws/client_types.py b/localstack-core/localstack/utils/aws/client_types.py index 0c6e1ce4c7501..1fd9f3a84df5e 100644 --- a/localstack-core/localstack/utils/aws/client_types.py +++ b/localstack-core/localstack/utils/aws/client_types.py @@ -110,6 +110,7 @@ from mypy_boto3_timestream_query import TimestreamQueryClient from mypy_boto3_timestream_write import TimestreamWriteClient from mypy_boto3_transcribe import TranscribeServiceClient + from mypy_boto3_verifiedpermissions import VerifiedPermissionsClient from mypy_boto3_wafv2 import WAFV2Client from mypy_boto3_xray import XRayClient @@ -259,6 +260,9 @@ class TypedServiceClientFactory(abc.ABC): "TimestreamWriteClient", "MetadataRequestInjector[TimestreamWriteClient]" ] transcribe: Union["TranscribeServiceClient", "MetadataRequestInjector[TranscribeServiceClient]"] + verifiedpermissions: Union[ + "VerifiedPermissionsClient", "MetadataRequestInjector[VerifiedPermissionsClient]" + ] wafv2: Union["WAFV2Client", "MetadataRequestInjector[WAFV2Client]"] xray: Union["XRayClient", "MetadataRequestInjector[XRayClient]"] diff --git a/pyproject.toml b/pyproject.toml index 9eaa72e897dc2..77d541e4fef9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codebuild,codecommit,codeconnections,codedeploy,codepipeline,codestar-connections,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,pinpoint,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codebuild,codecommit,codeconnections,codedeploy,codepipeline,codestar-connections,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,pinpoint,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,verifiedpermissions,wafv2,xray]", ] [tool.setuptools] diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 768a090aa10e0..ed997c151c141 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -460,6 +460,8 @@ mypy-boto3-timestream-write==1.37.0 # via boto3-stubs mypy-boto3-transcribe==1.37.5 # via boto3-stubs +mypy-boto3-verifiedpermissions==1.37.0 + # via boto3-stubs mypy-boto3-wafv2==1.37.21 # via boto3-stubs mypy-boto3-xray==1.37.0 @@ -777,6 +779,7 @@ typing-extensions==4.13.0 # mypy-boto3-timestream-query # mypy-boto3-timestream-write # mypy-boto3-transcribe + # mypy-boto3-verifiedpermissions # mypy-boto3-wafv2 # mypy-boto3-xray # pydantic From 0c25f7259dedb244af214607c4f2dbaaa250c78f Mon Sep 17 00:00:00 2001 From: Jim Wilkinson Date: Fri, 4 Apr 2025 09:31:35 +0100 Subject: [PATCH 022/108] Fix subnet tags going missing (#12459) This fixes a recent regression whereby the tags dict was inadvertently altered as part of processing to determine if a custom id was specified. When passed to the moto library, the tags were not in the expected format and ignored. This fix preserves the original format and adds a test case. Tags do not form part of the SG, VPC and subnet objects in moto. Instead they are stored in a separate dict in the EC2 backend. This means that if the object id is being changed, the tags need to have their key updated in the tags dict. Added cleanup processing to subnet and SG test cases. --- .../localstack/services/ec2/patches.py | 34 ++++++++++- tests/aws/services/ec2/test_ec2.py | 61 ++++++++++++++++++- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/services/ec2/patches.py b/localstack-core/localstack/services/ec2/patches.py index d9db4cad11e08..d26d94a3df83b 100644 --- a/localstack-core/localstack/services/ec2/patches.py +++ b/localstack-core/localstack/services/ec2/patches.py @@ -78,15 +78,22 @@ def ec2_create_subnet( tags: Optional[dict[str, str]] = None, **kwargs, ): + # Patch this method so that we can create a subnet with a specific "custom" + # ID. The custom ID that we will use is contained within a special tag. vpc_id: str = args[0] if len(args) >= 1 else kwargs["vpc_id"] cidr_block: str = args[1] if len(args) >= 1 else kwargs["cidr_block"] resource_identifier = SubnetIdentifier( self.account_id, self.region_name, vpc_id, cidr_block ) - # tags has the format: {"subnet": {"Key": ..., "Value": ...}} + + # tags has the format: {"subnet": {"Key": ..., "Value": ...}}, but we need + # to pass this to the generate method as {"Key": ..., "Value": ...}. Take + # care not to alter the original tags dict otherwise moto will not be able + # to understand it. + subnet_tags = None if tags is not None: - tags = tags.get("subnet", tags) - custom_id = resource_identifier.generate(tags=tags) + subnet_tags = tags.get("subnet", tags) + custom_id = resource_identifier.generate(tags=subnet_tags) if custom_id: # Check if custom id is unique within a given VPC @@ -102,9 +109,16 @@ def ec2_create_subnet( if custom_id: # Remove the subnet from the default dict and add it back with the custom id self.subnets[availability_zone].pop(result.id) + old_id = result.id result.id = custom_id self.subnets[availability_zone][custom_id] = result + # Tags are not stored in the Subnet object, but instead stored in a separate + # dict in the EC2 backend, keyed by subnet id. That therefore requires + # updating as well. + if old_id in self.tags: + self.tags[custom_id] = self.tags.pop(old_id) + # Return the subnet with the patched custom id return result @@ -132,9 +146,16 @@ def ec2_create_security_group( if custom_id: # Remove the security group from the default dict and add it back with the custom id self.groups[result.vpc_id].pop(result.group_id) + old_id = result.group_id result.group_id = result.id = custom_id self.groups[result.vpc_id][custom_id] = result + # Tags are not stored in the Security Group object, but instead are stored in a + # separate dict in the EC2 backend, keyed by id. That therefore requires + # updating as well. + if old_id in self.tags: + self.tags[custom_id] = self.tags.pop(old_id) + return result @patch(ec2_models.vpcs.VPCBackend.create_vpc) @@ -175,9 +196,16 @@ def ec2_create_vpc( # Remove the VPC from the default dict and add it back with the custom id self.vpcs.pop(vpc_id) + old_id = result.id result.id = custom_id self.vpcs[custom_id] = result + # Tags are not stored in the VPC object, but instead stored in a separate + # dict in the EC2 backend, keyed by VPC id. That therefore requires + # updating as well. + if old_id in self.tags: + self.tags[custom_id] = self.tags.pop(old_id) + # Create default network ACL, route table, and security group for custom ID VPC self.create_route_table( vpc_id=custom_id, diff --git a/tests/aws/services/ec2/test_ec2.py b/tests/aws/services/ec2/test_ec2.py index 0c32ba80386af..5ef1a8c15d743 100644 --- a/tests/aws/services/ec2/test_ec2.py +++ b/tests/aws/services/ec2/test_ec2.py @@ -460,6 +460,9 @@ def test_create_vpc_with_custom_id(self, aws_client, create_vpc): # Check if the custom ID is present in the describe_vpcs response as well vpc: dict = aws_client.ec2.describe_vpcs(VpcIds=[custom_id])["Vpcs"][0] assert vpc["VpcId"] == custom_id + assert len(vpc["Tags"]) == 1 + assert vpc["Tags"][0]["Key"] == TAG_KEY_CUSTOM_ID + assert vpc["Tags"][0]["Value"] == custom_id # Check if an duplicate custom ID exception is thrown if we try to recreate the VPC with the same custom ID with pytest.raises(ClientError) as e: @@ -479,7 +482,50 @@ def test_create_vpc_with_custom_id(self, aws_client, create_vpc): assert e.value.response["Error"]["Code"] == "InvalidVpc.DuplicateCustomId" @markers.aws.only_localstack - def test_create_subnet_with_custom_id(self, aws_client, create_vpc): + def test_create_subnet_with_tags(self, cleanups, aws_client, create_vpc): + # Create a VPC. + vpc: dict = create_vpc( + cidr_block="10.0.0.0/16", + tag_specifications=[ + { + "ResourceType": "vpc", + "Tags": [ + {"Key": "Name", "Value": "main-vpc"}, + ], + } + ], + ) + vpc_id: str = vpc["Vpc"]["VpcId"] + + # Create a subnet with a tag. + subnet: dict = aws_client.ec2.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.0.0/24", + TagSpecifications=[ + { + "ResourceType": "subnet", + "Tags": [ + {"Key": "Name", "Value": "main-subnet"}, + ], + } + ], + ) + cleanups.append(lambda: aws_client.ec2.delete_subnet(SubnetId=subnet["Subnet"]["SubnetId"])) + assert subnet["Subnet"]["VpcId"] == vpc_id + subnet_id: str = subnet["Subnet"]["SubnetId"] + + # Now check that the tags make it back on the describe subnets call. + subnet: dict = aws_client.ec2.describe_subnets( + SubnetIds=[subnet_id], + )["Subnets"][0] + assert subnet["SubnetId"] == subnet_id + assert subnet["VpcId"] == vpc_id + assert len(subnet["Tags"]) == 1 + assert subnet["Tags"][0]["Key"] == "Name" + assert subnet["Tags"][0]["Value"] == "main-subnet" + + @markers.aws.only_localstack + def test_create_subnet_with_custom_id(self, cleanups, aws_client, create_vpc): custom_id = random_subnet_id() # Create necessary VPC resource @@ -499,6 +545,7 @@ def test_create_subnet_with_custom_id(self, aws_client, create_vpc): } ], ) + cleanups.append(lambda: aws_client.ec2.delete_subnet(SubnetId=subnet["Subnet"]["SubnetId"])) assert subnet["Subnet"]["SubnetId"] == custom_id # Check if the custom ID is present in the describe_subnets response as well @@ -526,7 +573,7 @@ def test_create_subnet_with_custom_id(self, aws_client, create_vpc): assert e.value.response["Error"]["Code"] == "InvalidSubnet.DuplicateCustomId" @markers.aws.only_localstack - def test_create_subnet_with_custom_id_and_vpc_id(self, aws_client, create_vpc): + def test_create_subnet_with_custom_id_and_vpc_id(self, cleanups, aws_client, create_vpc): custom_subnet_id = random_subnet_id() custom_vpc_id = random_vpc_id() @@ -557,6 +604,7 @@ def test_create_subnet_with_custom_id_and_vpc_id(self, aws_client, create_vpc): } ], ) + cleanups.append(lambda: aws_client.ec2.delete_subnet(SubnetId=custom_subnet_id)) assert subnet["Subnet"]["SubnetId"] == custom_subnet_id # Check if the custom ID is present in the describe_subnets response as well @@ -565,9 +613,12 @@ def test_create_subnet_with_custom_id_and_vpc_id(self, aws_client, create_vpc): )["Subnets"][0] assert subnet["SubnetId"] == custom_subnet_id assert subnet["VpcId"] == custom_vpc_id + assert len(subnet["Tags"]) == 1 + assert subnet["Tags"][0]["Key"] == TAG_KEY_CUSTOM_ID + assert subnet["Tags"][0]["Value"] == custom_subnet_id @markers.aws.only_localstack - def test_create_security_group_with_custom_id(self, aws_client, create_vpc): + def test_create_security_group_with_custom_id(self, cleanups, aws_client, create_vpc): custom_id = random_security_group_id() # Create necessary VPC resource @@ -590,6 +641,7 @@ def test_create_security_group_with_custom_id(self, aws_client, create_vpc): } ], ) + cleanups.append(lambda: aws_client.ec2.delete_security_group(GroupId=custom_id)) assert security_group["GroupId"] == custom_id, ( f"Security group ID does not match custom ID: {security_group}" ) @@ -604,6 +656,9 @@ def test_create_security_group_with_custom_id(self, aws_client, create_vpc): (sg for sg in security_groups if sg["VpcId"] == vpc["Vpc"]["VpcId"]), None ) assert security_group["GroupId"] == custom_id + assert len(security_group["Tags"]) == 1 + assert security_group["Tags"][0]["Key"] == TAG_KEY_CUSTOM_ID + assert security_group["Tags"][0]["Value"] == custom_id # Check if a duplicate custom ID exception is thrown if we try to recreate the security group with the same custom ID with pytest.raises(ClientError) as e: From 8999cc442b59d653afbf99ccc4c19ddd6d6bc8df Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:07:38 +0530 Subject: [PATCH 023/108] KMS: fix RSA PSS signing issue for salt length (#12467) --- .../localstack/services/kms/models.py | 2 +- tests/aws/services/kms/test_kms.py | 57 ++++++++++++++++++- tests/aws/services/kms/test_kms.snapshot.json | 32 +++++++++++ .../aws/services/kms/test_kms.validation.json | 24 ++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/kms/models.py b/localstack-core/localstack/services/kms/models.py index fc96e9b4000e6..f923d7433e14b 100644 --- a/localstack-core/localstack/services/kms/models.py +++ b/localstack-core/localstack/services/kms/models.py @@ -476,7 +476,7 @@ def _construct_sign_verify_padding( if "PKCS" in signing_algorithm: return padding.PKCS1v15() elif "PSS" in signing_algorithm: - return padding.PSS(mgf=padding.MGF1(hasher), salt_length=padding.PSS.MAX_LENGTH) + return padding.PSS(mgf=padding.MGF1(hasher), salt_length=padding.PSS.DIGEST_LENGTH) else: LOG.warning("Unsupported padding in SigningAlgorithm '%s'", signing_algorithm) diff --git a/tests/aws/services/kms/test_kms.py b/tests/aws/services/kms/test_kms.py index fde9559a94658..2304b20cc0305 100644 --- a/tests/aws/services/kms/test_kms.py +++ b/tests/aws/services/kms/test_kms.py @@ -10,7 +10,7 @@ from botocore.config import Config from botocore.exceptions import ClientError from cryptography.hazmat.primitives import hashes, hmac, serialization -from cryptography.hazmat.primitives.asymmetric import ec, padding +from cryptography.hazmat.primitives.asymmetric import ec, padding, utils from cryptography.hazmat.primitives.serialization import load_der_public_key from localstack.services.kms.models import IV_LEN, Ciphertext, _serialize_ciphertext_blob @@ -26,6 +26,27 @@ def create_tags(**kwargs): return [{"TagKey": key, "TagValue": value} for key, value in kwargs.items()] +def get_signature_kwargs(signing_algorithm, message_type): + algo_map = { + "SHA_256": (hashes.SHA256(), 32), + "SHA_384": (hashes.SHA384(), 48), + "SHA_512": (hashes.SHA512(), 64), + } + hasher, salt = next((h, s) for k, (h, s) in algo_map.items() if k in signing_algorithm) + algorithm = utils.Prehashed(hasher) if message_type == "DIGEST" else hasher + kwargs = {} + + if signing_algorithm.startswith("ECDSA"): + kwargs["signature_algorithm"] = ec.ECDSA(algorithm) + elif signing_algorithm.startswith("RSA"): + if "PKCS" in signing_algorithm: + kwargs["padding"] = padding.PKCS1v15() + elif "PSS" in signing_algorithm: + kwargs["padding"] = padding.PSS(mgf=padding.MGF1(hasher), salt_length=salt) + kwargs["algorithm"] = algorithm + return kwargs + + @pytest.fixture(scope="class") def kms_client_for_region(aws_client_factory): def _kms_client( @@ -724,6 +745,40 @@ def test_sign_verify(self, kms_create_key, snapshot, key_spec, sign_algo, aws_cl ) assert exc.match("ValidationException") + @markers.aws.validated + @pytest.mark.parametrize( + "key_spec,sign_algo", + [ + ("RSA_2048", "RSASSA_PSS_SHA_256"), + ("RSA_2048", "RSASSA_PSS_SHA_384"), + ("RSA_2048", "RSASSA_PSS_SHA_512"), + ("RSA_4096", "RSASSA_PKCS1_V1_5_SHA_256"), + ("RSA_4096", "RSASSA_PKCS1_V1_5_SHA_512"), + ("ECC_NIST_P256", "ECDSA_SHA_256"), + ("ECC_NIST_P384", "ECDSA_SHA_384"), + ("ECC_SECG_P256K1", "ECDSA_SHA_256"), + ], + ) + def test_verify_salt_length(self, aws_client, kms_create_key, key_spec, sign_algo): + plaintext = b"test message !%$@ 1234567890" + + hash_algo = get_hash_algorithm(sign_algo) + hasher = getattr(hashlib, hash_algo.replace("_", "").lower()) + digest = hasher(plaintext).digest() + + key_id = kms_create_key(KeyUsage="SIGN_VERIFY", KeySpec=key_spec)["KeyId"] + public_key = aws_client.kms.get_public_key(KeyId=key_id)["PublicKey"] + key = load_der_public_key(public_key) + + kwargs = {"KeyId": key_id, "SigningAlgorithm": sign_algo} + + for msg_type, message in [("RAW", plaintext), ("DIGEST", digest)]: + signature = aws_client.kms.sign(MessageType=msg_type, Message=message, **kwargs)[ + "Signature" + ] + vargs = get_signature_kwargs(sign_algo, msg_type) + key.verify(signature=signature, data=message, **vargs) + @markers.aws.validated def test_invalid_key_usage(self, kms_create_key, aws_client): key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec="RSA_4096")["KeyId"] diff --git a/tests/aws/services/kms/test_kms.snapshot.json b/tests/aws/services/kms/test_kms.snapshot.json index f0231143655e4..e10807820c82a 100644 --- a/tests/aws/services/kms/test_kms.snapshot.json +++ b/tests/aws/services/kms/test_kms.snapshot.json @@ -2152,5 +2152,37 @@ } } } + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_256]": { + "recorded-date": "02-04-2025, 06:06:52", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_384]": { + "recorded-date": "02-04-2025, 06:06:54", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_512]": { + "recorded-date": "02-04-2025, 06:06:57", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_4096-RSASSA_PKCS1_V1_5_SHA_256]": { + "recorded-date": "02-04-2025, 06:06:59", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_4096-RSASSA_PKCS1_V1_5_SHA_512]": { + "recorded-date": "02-04-2025, 06:07:01", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_NIST_P256-ECDSA_SHA_256]": { + "recorded-date": "02-04-2025, 06:07:03", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_NIST_P384-ECDSA_SHA_384]": { + "recorded-date": "02-04-2025, 06:07:06", + "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_SECG_P256K1-ECDSA_SHA_256]": { + "recorded-date": "02-04-2025, 06:07:08", + "recorded-content": {} } } diff --git a/tests/aws/services/kms/test_kms.validation.json b/tests/aws/services/kms/test_kms.validation.json index 3aebce4a8142c..3dfb4c2682b53 100644 --- a/tests/aws/services/kms/test_kms.validation.json +++ b/tests/aws/services/kms/test_kms.validation.json @@ -275,6 +275,30 @@ "tests/aws/services/kms/test_kms.py::TestKMS::test_update_key_description": { "last_validated_date": "2024-04-11T15:53:46+00:00" }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_NIST_P256-ECDSA_SHA_256]": { + "last_validated_date": "2025-04-02T06:07:03+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_NIST_P384-ECDSA_SHA_384]": { + "last_validated_date": "2025-04-02T06:07:05+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_SECG_P256K1-ECDSA_SHA_256]": { + "last_validated_date": "2025-04-02T06:07:08+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_256]": { + "last_validated_date": "2025-04-02T06:06:52+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_384]": { + "last_validated_date": "2025-04-02T06:06:54+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_2048-RSASSA_PSS_SHA_512]": { + "last_validated_date": "2025-04-02T06:06:56+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_4096-RSASSA_PKCS1_V1_5_SHA_256]": { + "last_validated_date": "2025-04-02T06:06:58+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[RSA_4096-RSASSA_PKCS1_V1_5_SHA_512]": { + "last_validated_date": "2025-04-02T06:07:01+00:00" + }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key": { "last_validated_date": "2024-04-11T15:54:32+00:00" }, From 727155eaa76367684a7b1878ef5c58275dbaa3a3 Mon Sep 17 00:00:00 2001 From: mabuaisha Date: Fri, 4 Apr 2025 15:15:30 +0200 Subject: [PATCH 024/108] =?UTF-8?q?Secret=20Manager:=20Solve=20the=20issue?= =?UTF-8?q?=20for=20rotate=20secret=20after=20sub-sequent=20r=E2=80=A6=20(?= =?UTF-8?q?#12391)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/secretsmanager/provider.py | 17 +- .../testing/snapshots/transformer_utility.py | 13 ++ .../functions/lambda_rotate_secret.py | 11 + .../secretsmanager/test_secretsmanager.py | 184 +++++++++++----- .../test_secretsmanager.snapshot.json | 203 ++++++++++++++++-- .../test_secretsmanager.validation.json | 10 +- 6 files changed, 369 insertions(+), 69 deletions(-) diff --git a/localstack-core/localstack/services/secretsmanager/provider.py b/localstack-core/localstack/services/secretsmanager/provider.py index efefe6220819d..5838732f2c4b0 100644 --- a/localstack-core/localstack/services/secretsmanager/provider.py +++ b/localstack-core/localstack/services/secretsmanager/provider.py @@ -729,17 +729,28 @@ def backend_rotate_secret( if not self._is_valid_identifier(secret_id): raise SecretNotFoundException() - if self.secrets[secret_id].is_deleted(): + secret = self.secrets[secret_id] + if secret.is_deleted(): raise InvalidRequestException( "An error occurred (InvalidRequestException) when calling the RotateSecret operation: You tried to \ perform the operation on a secret that's currently marked deleted." ) + # Resolve rotation_lambda_arn and fallback to previous value if its missing + # from the current request + rotation_lambda_arn = rotation_lambda_arn or secret.rotation_lambda_arn + if not rotation_lambda_arn: + raise InvalidRequestException( + "No Lambda rotation function ARN is associated with this secret." + ) if rotation_lambda_arn: if len(rotation_lambda_arn) > 2048: msg = "RotationLambdaARN must <= 2048 characters long." raise InvalidParameterException(msg) + # In case rotation_period is not provided, resolve auto_rotate_after_days + # and fallback to previous value if its missing from the current request. + rotation_period = secret.auto_rotate_after_days or 0 if rotation_rules: if rotation_days in rotation_rules: rotation_period = rotation_rules[rotation_days] @@ -753,8 +764,6 @@ def backend_rotate_secret( except Exception: raise ResourceNotFoundException("Lambda does not exist or could not be accessed") - secret = self.secrets[secret_id] - # The rotation function must end with the versions of the secret in # one of two states: # @@ -782,7 +791,7 @@ def backend_rotate_secret( pass secret.rotation_lambda_arn = rotation_lambda_arn - secret.auto_rotate_after_days = rotation_rules.get(rotation_days, 0) + secret.auto_rotate_after_days = rotation_period if secret.auto_rotate_after_days > 0: wait_interval_s = int(rotation_period) * 86400 secret.next_rotation_date = int(time.time()) + wait_interval_s diff --git a/localstack-core/localstack/testing/snapshots/transformer_utility.py b/localstack-core/localstack/testing/snapshots/transformer_utility.py index 77a9bdfc6e0b5..7d2d73c844dbb 100644 --- a/localstack-core/localstack/testing/snapshots/transformer_utility.py +++ b/localstack-core/localstack/testing/snapshots/transformer_utility.py @@ -648,6 +648,19 @@ def secretsmanager_api(): ), "version_uuid", ), + KeyValueBasedTransformer( + lambda k, v: ( + v + if ( + isinstance(k, str) + and k == "RotationLambdaARN" + and isinstance(v, str) + and re.match(PATTERN_ARN, v) + ) + else None + ), + "lambda-arn", + ), SortingTransformer("VersionStages"), SortingTransformer("Versions", lambda e: e.get("CreatedDate")), ] diff --git a/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py b/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py index 97dcf17736256..ccfebb8621bf3 100644 --- a/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py +++ b/tests/aws/services/secretsmanager/functions/lambda_rotate_secret.py @@ -224,3 +224,14 @@ def finish_secret(service_client, arn, token): token, arn, ) + if "AWSPENDING" in metadata["VersionIdsToStages"].get(token, []): + service_client.update_secret_version_stage( + SecretId=arn, + VersionStage="AWSPENDING", + RemoveFromVersionId=token, + ) + logger.info( + "finishSecret: Successfully removed AWSPENDING stage from version %s for secret %s.", + token, + arn, + ) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index e8c7456156047..7a91414c6879e 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -70,6 +70,62 @@ def sm_snapshot(self, snapshot): snapshot.add_transformers_list(snapshot.transform.secretsmanager_api()) return snapshot + @pytest.fixture + def setup_invalid_rotation_secret(self, secret_name, aws_client, account_id, sm_snapshot): + def _setup(invalid_arn: str | None): + create_secret = aws_client.secretsmanager.create_secret( + Name=secret_name, SecretString="init" + ) + sm_snapshot.add_transformer( + sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) + ) + sm_snapshot.match("create_secret", create_secret) + rotation_config = { + "SecretId": secret_name, + "RotationRules": { + "AutomaticallyAfterDays": 1, + }, + } + if invalid_arn: + rotation_config["RotationLambdaARN"] = invalid_arn + aws_client.secretsmanager.rotate_secret(**rotation_config) + + return _setup + + @pytest.fixture + def setup_rotation_secret( + self, + sm_snapshot, + secret_name, + create_secret, + create_lambda_function, + aws_client, + ): + cre_res = create_secret( + Name=secret_name, + SecretString="my_secret", + Description="testing rotation of secrets", + ) + + sm_snapshot.add_transformers_list( + sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) + ) + + function_name = f"s-{short_uid()}" + function_arn = create_lambda_function( + handler_file=TEST_LAMBDA_ROTATE_SECRET, + func_name=function_name, + runtime=Runtime.python3_12, + )["CreateFunctionResponse"]["FunctionArn"] + + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId="secretsManagerPermission", + Action="lambda:InvokeFunction", + Principal="secretsmanager.amazonaws.com", + ) + return cre_res["VersionId"], function_arn + @staticmethod def _wait_created_is_listed(client, secret_id: str): def _is_secret_in_list(): @@ -527,49 +583,27 @@ def test_rotate_secret_with_lambda_success( create_secret, create_lambda_function, aws_client, + setup_rotation_secret, rotate_immediately, ): """ Tests secret rotation via a lambda function. Parametrization ensures we test the default behavior which is an immediate rotation. """ - cre_res = create_secret( - Name=secret_name, - SecretString="my_secret", - Description="testing rotation of secrets", - ) - - sm_snapshot.add_transformer( - sm_snapshot.transform.key_value("RotationLambdaARN", "lambda-arn") - ) - sm_snapshot.add_transformers_list( - sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) - ) - - function_name = f"s-{short_uid()}" - function_arn = create_lambda_function( - handler_file=TEST_LAMBDA_ROTATE_SECRET, - func_name=function_name, - runtime=Runtime.python3_12, - )["CreateFunctionResponse"]["FunctionArn"] + rotation_config = { + "RotationRules": {"AutomaticallyAfterDays": 1}, + } + if rotate_immediately: + rotation_config["RotateImmediately"] = rotate_immediately + initial_secret_version, function_arn = setup_rotation_secret - aws_client.lambda_.add_permission( - FunctionName=function_name, - StatementId="secretsManagerPermission", - Action="lambda:InvokeFunction", - Principal="secretsmanager.amazonaws.com", - ) + rotation_config = rotation_config or {} + if function_arn: + rotation_config["RotationLambdaARN"] = function_arn - rotation_kwargs = {} - if rotate_immediately is not None: - rotation_kwargs["RotateImmediately"] = rotate_immediately rot_res = aws_client.secretsmanager.rotate_secret( SecretId=secret_name, - RotationLambdaARN=function_arn, - RotationRules={ - "AutomaticallyAfterDays": 1, - }, - **rotation_kwargs, + **rotation_config, ) sm_snapshot.match("rotate_secret_immediately", rot_res) @@ -585,31 +619,75 @@ def test_rotate_secret_with_lambda_success( sm_snapshot.match("list_secret_versions_rotated_1", list_secret_versions_1) + # As a result of the Lambda invocations. current version should be + # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` + assert response["VersionIdsToStages"][initial_secret_version] == ["AWSPREVIOUS"] + assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + + @markers.snapshot.skip_snapshot_verify( + paths=["$..VersionIdsToStages", "$..Versions", "$..VersionId"] + ) + @markers.aws.validated + def test_rotate_secret_multiple_times_with_lambda_success( + self, + sm_snapshot, + secret_name, + create_secret, + create_lambda_function, + aws_client, + setup_rotation_secret, + ): + secret_initial_version, function_arn = setup_rotation_secret + runs_config = { + 1: { + "RotationRules": {"AutomaticallyAfterDays": 1}, + "RotateImmediately": True, + "RotationLambdaARN": function_arn, + }, + 2: {}, + } + + for index in range(1, 3): + rotation_config = runs_config[index] + + rot_res = aws_client.secretsmanager.rotate_secret( + SecretId=secret_name, + **rotation_config, + ) + + sm_snapshot.match(f"rotate_secret_immediately_{index}", rot_res) + + self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) + + response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match(f"describe_secret_rotated_{index}", response) + + list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name + ) + + sm_snapshot.match(f"list_secret_versions_rotated_1_{index}", list_secret_versions_1) + + # As a result of the Lambda invocations. current version should be + # pointed to `AWSCURRENT` & previous version to `AWSPREVIOUS` + assert response["VersionIdsToStages"][secret_initial_version] == ["AWSPREVIOUS"] + assert response["VersionIdsToStages"][rot_res["VersionId"]] == ["AWSCURRENT"] + + secret_initial_version = aws_client.secretsmanager.get_secret_value( + SecretId=secret_name + )["VersionId"] + @markers.snapshot.skip_snapshot_verify(paths=["$..Error", "$..Message"]) @markers.aws.validated def test_rotate_secret_invalid_lambda_arn( - self, secret_name, aws_client, account_id, sm_snapshot + self, setup_invalid_rotation_secret, aws_client, sm_snapshot, secret_name, account_id ): - create_secret = aws_client.secretsmanager.create_secret( - Name=secret_name, SecretString="init" - ) - sm_snapshot.add_transformer( - sm_snapshot.transform.secretsmanager_secret_id_arn(create_secret, 0) - ) - sm_snapshot.match("create_secret", create_secret) - region_name = aws_client.secretsmanager.meta.region_name invalid_arn = ( f"arn:aws:lambda:{region_name}:{account_id}:function:rotate_secret_invalid_lambda_arn" ) with pytest.raises(Exception) as e: - aws_client.secretsmanager.rotate_secret( - SecretId=secret_name, - RotationLambdaARN=invalid_arn, - RotationRules={ - "AutomaticallyAfterDays": 1, - }, - ) + setup_invalid_rotation_secret(invalid_arn) sm_snapshot.match("rotate_secret_invalid_arn_exc", e.value.response) describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_name) @@ -618,6 +696,14 @@ def test_rotate_secret_invalid_lambda_arn( assert "RotationRules" not in describe_secret assert "RotationLambdaARN" not in describe_secret + @markers.aws.validated + def test_first_rotate_secret_with_missing_lambda_arn( + self, setup_invalid_rotation_secret, sm_snapshot + ): + with pytest.raises(Exception) as e: + setup_invalid_rotation_secret(None) + sm_snapshot.match("rotate_secret_no_arn_exc", e.value.response) + @markers.aws.validated def test_put_secret_value_with_version_stages(self, sm_snapshot, secret_name, aws_client): secret_string_v0: str = "secret_string_v0" diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 003987e7c32e2..8e52ed68a419c 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -3687,12 +3687,12 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "recorded-date": "28-03-2024, 06:58:46", + "recorded-date": "30-03-2025, 11:45:42", "recorded-content": { "rotate_secret_immediately": { "ARN": "arn::secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3714,11 +3714,10 @@ }, "VersionIdsToStages": { "": [ - "AWSCURRENT", - "AWSPENDING" + "AWSPREVIOUS" ], "": [ - "AWSPREVIOUS" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3736,7 +3735,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3746,10 +3745,9 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] } ], @@ -3761,7 +3759,7 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "recorded-date": "28-03-2024, 06:58:58", + "recorded-date": "30-03-2025, 11:45:54", "recorded-content": { "rotate_secret_immediately": { "ARN": "arn::secretsmanager::111111111111:secret:", @@ -3791,8 +3789,7 @@ "AWSPREVIOUS" ], "": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] }, "ResponseMetadata": { @@ -3822,8 +3819,7 @@ ], "VersionId": "", "VersionStages": [ - "AWSCURRENT", - "AWSPENDING" + "AWSCURRENT" ] } ], @@ -4586,5 +4582,184 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { + "recorded-date": "27-03-2025, 16:33:46", + "recorded-content": { + "create_secret": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "rotate_secret_no_arn_exc": { + "Error": { + "Code": "InvalidRequestException", + "Message": "No Lambda rotation function ARN is associated with this secret." + }, + "Message": "No Lambda rotation function ARN is associated with this secret.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "describe_secret": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "VersionIdsToStages": { + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success": { + "recorded-date": "29-03-2025, 09:40:15", + "recorded-content": { + "rotate_secret_immediately_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSPREVIOUS" + ], + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_1": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "rotate_secret_immediately_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSPREVIOUS" + ], + "": [ + "AWSCURRENT" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_versions_rotated_1_2": { + "ARN": "arn::secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "LastAccessedDate": "datetime", + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index a85ca0d9e3e4a..d44fb5cb56bc5 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -41,6 +41,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_exp_raised_on_creation_of_secret_scheduled_for_deletion": { "last_validated_date": "2024-03-15T08:13:16+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_first_rotate_secret_with_missing_lambda_arn": { + "last_validated_date": "2025-03-27T16:33:46+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_force_delete_deleted_secret": { "last_validated_date": "2024-10-11T14:33:45+00:00" }, @@ -101,14 +104,17 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_invalid_lambda_arn": { "last_validated_date": "2024-03-15T10:11:13+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_multiple_times_with_lambda_success": { + "last_validated_date": "2025-03-29T09:40:15+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success": { "last_validated_date": "2024-03-15T08:12:22+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "last_validated_date": "2024-03-28T06:58:56+00:00" + "last_validated_date": "2025-03-30T11:45:54+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "last_validated_date": "2024-03-28T06:58:44+00:00" + "last_validated_date": "2025-03-30T11:45:41+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": { "last_validated_date": "2024-03-15T08:14:33+00:00" From ac76ec1eb7c84771de7e030372517817a173bcb2 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 4 Apr 2025 13:50:30 +0000 Subject: [PATCH 025/108] Docker: Improve error messages around port-bound check (#12477) --- localstack-core/localstack/utils/docker_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/utils/docker_utils.py b/localstack-core/localstack/utils/docker_utils.py index bab738135f053..9ff5f57134ca6 100644 --- a/localstack-core/localstack/utils/docker_utils.py +++ b/localstack-core/localstack/utils/docker_utils.py @@ -156,14 +156,14 @@ def container_ports_can_be_bound( except Exception as e: if "port is already allocated" not in str(e) and "address already in use" not in str(e): LOG.warning( - "Unexpected error when attempting to determine container port status: %s", e + "Unexpected error when attempting to determine container port status", exc_info=e ) return False # TODO(srw): sometimes the command output from the docker container is "None", particularly when this function is # invoked multiple times consecutively. Work out why. if to_str(result[0] or "").strip() != "test123": LOG.warning( - "Unexpected output when attempting to determine container port status: %s", result[0] + "Unexpected output when attempting to determine container port status: %s", result ) return True From 30a0d9241db4d7a945b1a800d44a7afce6006158 Mon Sep 17 00:00:00 2001 From: Greg Furman <31275503+gregfurman@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:37:48 +0200 Subject: [PATCH 026/108] [ESM] Re-initialize shards when NextShardIterator value is empty (#12483) --- .../lambda_/event_source_mapping/pollers/dynamodb_poller.py | 2 ++ .../lambda_/event_source_mapping/pollers/kinesis_poller.py | 2 ++ .../lambda_/event_source_mapping/pollers/stream_poller.py | 6 ++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py index d69d26baeb87a..2a8e793945c42 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py @@ -61,6 +61,8 @@ def initialize_shards(self): **kwargs, ) shards[shard_id] = get_shard_iterator_response["ShardIterator"] + + LOG.debug("Event source %s has %d shards.", self.source_arn, len(self.shards)) return shards def stream_arn_param(self) -> dict: diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py index aae917e84db2a..e2dc19b74b012 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py @@ -84,6 +84,8 @@ def initialize_shards(self) -> dict[str, str]: **kwargs, ) shards[shard_id] = get_shard_iterator_response["ShardIterator"] + + LOG.debug("Event source %s has %d shards.", self.source_arn, len(self.shards)) return shards def stream_arn_param(self) -> dict: diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py index 158f108592a78..c489fa87a9ed6 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py @@ -154,7 +154,6 @@ def poll_events(self): LOG.debug("No shards found for %s.", self.source_arn) raise EmptyPollResultsException(service=self.event_source(), source_arn=self.source_arn) else: - LOG.debug("Event source %s has %d shards.", self.source_arn, len(self.shards)) # Remove all shard batchers without corresponding shards for shard_id in self.shard_batcher.keys() - self.shards.keys(): self.shard_batcher.pop(shard_id, None) @@ -185,7 +184,10 @@ def poll_events(self): def poll_events_from_shard(self, shard_id: str, shard_iterator: str): get_records_response = self.get_records(shard_iterator) records: list[dict] = get_records_response.get("Records", []) - next_shard_iterator = get_records_response["NextShardIterator"] + if not (next_shard_iterator := get_records_response.get("NextShardIterator")): + # If the next shard iterator is None, we can assume the shard is closed or + # has expired on the DynamoDB Local server, hence we should re-initialize. + self.shards = self.initialize_shards() # We cannot reliably back-off when no records found since an iterator # may have to move multiple times until records are returned. From 7dc21bd8d6d1ae7fd502a4e69aa6d1f8808a8ccf Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Mon, 7 Apr 2025 06:27:59 +0000 Subject: [PATCH 027/108] Admin: Update License (#12489) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77d541e4fef9e..45e6ae9921193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ authors = [ { name = "LocalStack Contributors", email = "info@localstack.cloud" } ] description = "The core library and runtime of LocalStack" +license = "Apache-2.0" requires-python = ">=3.9" dependencies = [ "build", @@ -31,7 +32,6 @@ dynamic = ["version"] classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", - "License-Expression :: OSI Approved :: Apache Software License", "Topic :: Internet", "Topic :: Software Development :: Testing", "Topic :: System :: Emulators", From 72ed3cd9374533911543ba62f5d74ef8bd351398 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:41:33 +0200 Subject: [PATCH 028/108] Update ASF APIs, update events provider signature (#12490) Co-authored-by: LocalStack Bot Co-authored-by: Alexander Rashed --- .../localstack/aws/api/ec2/__init__.py | 597 ++++++++++++++++++ .../localstack/aws/api/events/__init__.py | 30 +- .../localstack/aws/api/route53/__init__.py | 2 + .../localstack/aws/api/s3control/__init__.py | 89 +++ .../localstack/aws/api/transcribe/__init__.py | 1 + .../localstack/services/events/provider.py | 7 +- pyproject.toml | 4 +- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +- requirements-runtime.txt | 6 +- requirements-test.txt | 6 +- requirements-typehint.txt | 6 +- 12 files changed, 729 insertions(+), 29 deletions(-) diff --git a/localstack-core/localstack/aws/api/ec2/__init__.py b/localstack-core/localstack/aws/api/ec2/__init__.py index 41c82c1020408..f15d77efc00ad 100644 --- a/localstack-core/localstack/aws/api/ec2/__init__.py +++ b/localstack-core/localstack/aws/api/ec2/__init__.py @@ -299,6 +299,10 @@ RetentionPeriodResponseDays = int RoleId = str RouteGatewayId = str +RouteServerEndpointId = str +RouteServerId = str +RouteServerMaxResults = int +RouteServerPeerId = str RouteTableAssociationId = str RouteTableId = str RunInstancesUserData = str @@ -3016,6 +3020,9 @@ class ResourceType(StrEnum): verified_access_trust_provider = "verified-access-trust-provider" vpn_connection_device_type = "vpn-connection-device-type" vpc_block_public_access_exclusion = "vpc-block-public-access-exclusion" + route_server = "route-server" + route_server_endpoint = "route-server-endpoint" + route_server_peer = "route-server-peer" ipam_resource_discovery = "ipam-resource-discovery" ipam_resource_discovery_association = "ipam-resource-discovery-association" instance_connect_endpoint = "instance-connect-endpoint" @@ -3034,6 +3041,85 @@ class RouteOrigin(StrEnum): EnableVgwRoutePropagation = "EnableVgwRoutePropagation" +class RouteServerAssociationState(StrEnum): + associating = "associating" + associated = "associated" + disassociating = "disassociating" + + +class RouteServerBfdState(StrEnum): + up = "up" + down = "down" + + +class RouteServerBgpState(StrEnum): + up = "up" + down = "down" + + +class RouteServerEndpointState(StrEnum): + pending = "pending" + available = "available" + deleting = "deleting" + deleted = "deleted" + failing = "failing" + failed = "failed" + delete_failed = "delete-failed" + + +class RouteServerPeerLivenessMode(StrEnum): + bfd = "bfd" + bgp_keepalive = "bgp-keepalive" + + +class RouteServerPeerState(StrEnum): + pending = "pending" + available = "available" + deleting = "deleting" + deleted = "deleted" + failing = "failing" + failed = "failed" + + +class RouteServerPersistRoutesAction(StrEnum): + enable = "enable" + disable = "disable" + reset = "reset" + + +class RouteServerPersistRoutesState(StrEnum): + enabling = "enabling" + enabled = "enabled" + resetting = "resetting" + disabling = "disabling" + disabled = "disabled" + modifying = "modifying" + + +class RouteServerPropagationState(StrEnum): + pending = "pending" + available = "available" + deleting = "deleting" + + +class RouteServerRouteInstallationStatus(StrEnum): + installed = "installed" + rejected = "rejected" + + +class RouteServerRouteStatus(StrEnum): + in_rib = "in-rib" + in_fib = "in-fib" + + +class RouteServerState(StrEnum): + pending = "pending" + available = "available" + modifying = "modifying" + deleting = "deleting" + deleted = "deleted" + + class RouteState(StrEnum): active = "active" blackhole = "blackhole" @@ -3632,6 +3718,8 @@ class VpcEncryptionControlState(StrEnum): deleting = "deleting" deleted = "deleted" available = "available" + creating = "creating" + delete_failed = "delete-failed" class VpcEndpointType(StrEnum): @@ -4527,6 +4615,7 @@ class ApplySecurityGroupsToClientVpnTargetNetworkResult(TypedDict, total=False): ArchitectureTypeList = List[ArchitectureType] ArchitectureTypeSet = List[ArchitectureType] ArnList = List[ResourceArn] +AsPath = List[String] class AsnAuthorizationContext(TypedDict, total=False): @@ -4793,6 +4882,22 @@ class AssociateNatGatewayAddressResult(TypedDict, total=False): NatGatewayAddresses: Optional[NatGatewayAddressList] +class AssociateRouteServerRequest(ServiceRequest): + RouteServerId: RouteServerId + VpcId: VpcId + DryRun: Optional[Boolean] + + +class RouteServerAssociation(TypedDict, total=False): + RouteServerId: Optional[RouteServerId] + VpcId: Optional[VpcId] + State: Optional[RouteServerAssociationState] + + +class AssociateRouteServerResult(TypedDict, total=False): + RouteServerAssociation: Optional[RouteServerAssociation] + + class AssociateRouteTableRequest(ServiceRequest): GatewayId: Optional[RouteGatewayId] DryRun: Optional[Boolean] @@ -5459,6 +5564,7 @@ class BlockPublicAccessStates(TypedDict, total=False): BootModeTypeList = List[BootModeType] +BoxedLong = int BundleIdStringList = List[BundleId] @@ -8477,6 +8583,102 @@ class CreateRouteResult(TypedDict, total=False): Return: Optional[Boolean] +class CreateRouteServerEndpointRequest(ServiceRequest): + RouteServerId: RouteServerId + SubnetId: SubnetId + ClientToken: Optional[String] + DryRun: Optional[Boolean] + TagSpecifications: Optional[TagSpecificationList] + + +class RouteServerEndpoint(TypedDict, total=False): + RouteServerId: Optional[RouteServerId] + RouteServerEndpointId: Optional[RouteServerEndpointId] + VpcId: Optional[VpcId] + SubnetId: Optional[SubnetId] + EniId: Optional[NetworkInterfaceId] + EniAddress: Optional[String] + State: Optional[RouteServerEndpointState] + FailureReason: Optional[String] + Tags: Optional[TagList] + + +class CreateRouteServerEndpointResult(TypedDict, total=False): + RouteServerEndpoint: Optional[RouteServerEndpoint] + + +class RouteServerBgpOptionsRequest(TypedDict, total=False): + PeerAsn: Long + PeerLivenessDetection: Optional[RouteServerPeerLivenessMode] + + +class CreateRouteServerPeerRequest(ServiceRequest): + RouteServerEndpointId: RouteServerEndpointId + PeerAddress: String + BgpOptions: RouteServerBgpOptionsRequest + DryRun: Optional[Boolean] + TagSpecifications: Optional[TagSpecificationList] + + +class RouteServerBfdStatus(TypedDict, total=False): + Status: Optional[RouteServerBfdState] + + +class RouteServerBgpStatus(TypedDict, total=False): + Status: Optional[RouteServerBgpState] + + +class RouteServerBgpOptions(TypedDict, total=False): + PeerAsn: Optional[Long] + PeerLivenessDetection: Optional[RouteServerPeerLivenessMode] + + +class RouteServerPeer(TypedDict, total=False): + RouteServerPeerId: Optional[RouteServerPeerId] + RouteServerEndpointId: Optional[RouteServerEndpointId] + RouteServerId: Optional[RouteServerId] + VpcId: Optional[VpcId] + SubnetId: Optional[SubnetId] + State: Optional[RouteServerPeerState] + FailureReason: Optional[String] + EndpointEniId: Optional[NetworkInterfaceId] + EndpointEniAddress: Optional[String] + PeerAddress: Optional[String] + BgpOptions: Optional[RouteServerBgpOptions] + BgpStatus: Optional[RouteServerBgpStatus] + BfdStatus: Optional[RouteServerBfdStatus] + Tags: Optional[TagList] + + +class CreateRouteServerPeerResult(TypedDict, total=False): + RouteServerPeer: Optional[RouteServerPeer] + + +class CreateRouteServerRequest(ServiceRequest): + AmazonSideAsn: Long + ClientToken: Optional[String] + DryRun: Optional[Boolean] + PersistRoutes: Optional[RouteServerPersistRoutesAction] + PersistRoutesDuration: Optional[BoxedLong] + SnsNotificationsEnabled: Optional[Boolean] + TagSpecifications: Optional[TagSpecificationList] + + +class RouteServer(TypedDict, total=False): + RouteServerId: Optional[RouteServerId] + AmazonSideAsn: Optional[Long] + State: Optional[RouteServerState] + Tags: Optional[TagList] + PersistRoutesState: Optional[RouteServerPersistRoutesState] + PersistRoutesDuration: Optional[BoxedLong] + SnsNotificationsEnabled: Optional[Boolean] + SnsTopicArn: Optional[String] + + +class CreateRouteServerResult(TypedDict, total=False): + RouteServer: Optional[RouteServer] + + class CreateRouteTableRequest(ServiceRequest): TagSpecifications: Optional[TagSpecificationList] ClientToken: Optional[String] @@ -10412,6 +10614,33 @@ class DeleteRouteRequest(ServiceRequest): DestinationIpv6CidrBlock: Optional[String] +class DeleteRouteServerEndpointRequest(ServiceRequest): + RouteServerEndpointId: RouteServerEndpointId + DryRun: Optional[Boolean] + + +class DeleteRouteServerEndpointResult(TypedDict, total=False): + RouteServerEndpoint: Optional[RouteServerEndpoint] + + +class DeleteRouteServerPeerRequest(ServiceRequest): + RouteServerPeerId: RouteServerPeerId + DryRun: Optional[Boolean] + + +class DeleteRouteServerPeerResult(TypedDict, total=False): + RouteServerPeer: Optional[RouteServerPeer] + + +class DeleteRouteServerRequest(ServiceRequest): + RouteServerId: RouteServerId + DryRun: Optional[Boolean] + + +class DeleteRouteServerResult(TypedDict, total=False): + RouteServer: Optional[RouteServer] + + class DeleteRouteTableRequest(ServiceRequest): DryRun: Optional[Boolean] RouteTableId: RouteTableId @@ -13748,6 +13977,63 @@ class DescribeReservedInstancesResult(TypedDict, total=False): ReservedInstances: Optional[ReservedInstancesList] +RouteServerEndpointIdsList = List[RouteServerEndpointId] + + +class DescribeRouteServerEndpointsRequest(ServiceRequest): + RouteServerEndpointIds: Optional[RouteServerEndpointIdsList] + NextToken: Optional[String] + MaxResults: Optional[RouteServerMaxResults] + Filters: Optional[FilterList] + DryRun: Optional[Boolean] + + +RouteServerEndpointsList = List[RouteServerEndpoint] + + +class DescribeRouteServerEndpointsResult(TypedDict, total=False): + RouteServerEndpoints: Optional[RouteServerEndpointsList] + NextToken: Optional[String] + + +RouteServerPeerIdsList = List[RouteServerPeerId] + + +class DescribeRouteServerPeersRequest(ServiceRequest): + RouteServerPeerIds: Optional[RouteServerPeerIdsList] + NextToken: Optional[String] + MaxResults: Optional[RouteServerMaxResults] + Filters: Optional[FilterList] + DryRun: Optional[Boolean] + + +RouteServerPeersList = List[RouteServerPeer] + + +class DescribeRouteServerPeersResult(TypedDict, total=False): + RouteServerPeers: Optional[RouteServerPeersList] + NextToken: Optional[String] + + +RouteServerIdsList = List[RouteServerId] + + +class DescribeRouteServersRequest(ServiceRequest): + RouteServerIds: Optional[RouteServerIdsList] + NextToken: Optional[String] + MaxResults: Optional[RouteServerMaxResults] + Filters: Optional[FilterList] + DryRun: Optional[Boolean] + + +RouteServersList = List[RouteServer] + + +class DescribeRouteServersResult(TypedDict, total=False): + RouteServers: Optional[RouteServersList] + NextToken: Optional[String] + + RouteTableIdStringList = List[RouteTableId] @@ -15593,6 +15879,22 @@ class DisableIpamOrganizationAdminAccountResult(TypedDict, total=False): Success: Optional[Boolean] +class DisableRouteServerPropagationRequest(ServiceRequest): + RouteServerId: RouteServerId + RouteTableId: RouteTableId + DryRun: Optional[Boolean] + + +class RouteServerPropagation(TypedDict, total=False): + RouteServerId: Optional[RouteServerId] + RouteTableId: Optional[RouteTableId] + State: Optional[RouteServerPropagationState] + + +class DisableRouteServerPropagationResult(TypedDict, total=False): + RouteServerPropagation: Optional[RouteServerPropagation] + + class DisableSerialConsoleAccessRequest(ServiceRequest): DryRun: Optional[Boolean] @@ -15747,6 +16049,16 @@ class DisassociateNatGatewayAddressResult(TypedDict, total=False): NatGatewayAddresses: Optional[NatGatewayAddressList] +class DisassociateRouteServerRequest(ServiceRequest): + RouteServerId: RouteServerId + VpcId: VpcId + DryRun: Optional[Boolean] + + +class DisassociateRouteServerResult(TypedDict, total=False): + RouteServerAssociation: Optional[RouteServerAssociation] + + class DisassociateRouteTableRequest(ServiceRequest): DryRun: Optional[Boolean] AssociationId: RouteTableAssociationId @@ -16037,6 +16349,16 @@ class EnableReachabilityAnalyzerOrganizationSharingResult(TypedDict, total=False ReturnValue: Optional[Boolean] +class EnableRouteServerPropagationRequest(ServiceRequest): + RouteServerId: RouteServerId + RouteTableId: RouteTableId + DryRun: Optional[Boolean] + + +class EnableRouteServerPropagationResult(TypedDict, total=False): + RouteServerPropagation: Optional[RouteServerPropagation] + + class EnableSerialConsoleAccessRequest(ServiceRequest): DryRun: Optional[Boolean] @@ -16866,6 +17188,68 @@ class GetReservedInstancesExchangeQuoteResult(TypedDict, total=False): ValidationFailureReason: Optional[String] +class GetRouteServerAssociationsRequest(ServiceRequest): + RouteServerId: RouteServerId + DryRun: Optional[Boolean] + + +RouteServerAssociationsList = List[RouteServerAssociation] + + +class GetRouteServerAssociationsResult(TypedDict, total=False): + RouteServerAssociations: Optional[RouteServerAssociationsList] + + +class GetRouteServerPropagationsRequest(ServiceRequest): + RouteServerId: RouteServerId + RouteTableId: Optional[RouteTableId] + DryRun: Optional[Boolean] + + +RouteServerPropagationsList = List[RouteServerPropagation] + + +class GetRouteServerPropagationsResult(TypedDict, total=False): + RouteServerPropagations: Optional[RouteServerPropagationsList] + + +class GetRouteServerRoutingDatabaseRequest(ServiceRequest): + RouteServerId: RouteServerId + NextToken: Optional[String] + MaxResults: Optional[RouteServerMaxResults] + DryRun: Optional[Boolean] + Filters: Optional[FilterList] + + +class RouteServerRouteInstallationDetail(TypedDict, total=False): + RouteTableId: Optional[RouteTableId] + RouteInstallationStatus: Optional[RouteServerRouteInstallationStatus] + RouteInstallationStatusReason: Optional[String] + + +RouteServerRouteInstallationDetails = List[RouteServerRouteInstallationDetail] + + +class RouteServerRoute(TypedDict, total=False): + RouteServerEndpointId: Optional[RouteServerEndpointId] + RouteServerPeerId: Optional[RouteServerPeerId] + RouteInstallationDetails: Optional[RouteServerRouteInstallationDetails] + RouteStatus: Optional[RouteServerRouteStatus] + Prefix: Optional[String] + AsPaths: Optional[AsPath] + Med: Optional[Integer] + NextHopIp: Optional[String] + + +RouteServerRouteList = List[RouteServerRoute] + + +class GetRouteServerRoutingDatabaseResult(TypedDict, total=False): + AreRoutesPersisted: Optional[Boolean] + Routes: Optional[RouteServerRouteList] + NextToken: Optional[String] + + class GetSecurityGroupsForVpcRequest(ServiceRequest): VpcId: VpcId NextToken: Optional[String] @@ -18112,6 +18496,18 @@ class ModifyReservedInstancesResult(TypedDict, total=False): ReservedInstancesModificationId: Optional[String] +class ModifyRouteServerRequest(ServiceRequest): + RouteServerId: RouteServerId + PersistRoutes: Optional[RouteServerPersistRoutesAction] + PersistRoutesDuration: Optional[BoxedLong] + SnsNotificationsEnabled: Optional[Boolean] + DryRun: Optional[Boolean] + + +class ModifyRouteServerResult(TypedDict, total=False): + RouteServer: Optional[RouteServer] + + class SecurityGroupRuleRequest(TypedDict, total=False): IpProtocol: Optional[String] FromPort: Optional[Integer] @@ -20095,6 +20491,17 @@ def associate_nat_gateway_address( ) -> AssociateNatGatewayAddressResult: raise NotImplementedError + @handler("AssociateRouteServer") + def associate_route_server( + self, + context: RequestContext, + route_server_id: RouteServerId, + vpc_id: VpcId, + dry_run: Boolean = None, + **kwargs, + ) -> AssociateRouteServerResult: + raise NotImplementedError + @handler("AssociateRouteTable") def associate_route_table( self, @@ -21177,6 +21584,47 @@ def create_route( ) -> CreateRouteResult: raise NotImplementedError + @handler("CreateRouteServer") + def create_route_server( + self, + context: RequestContext, + amazon_side_asn: Long, + client_token: String = None, + dry_run: Boolean = None, + persist_routes: RouteServerPersistRoutesAction = None, + persist_routes_duration: BoxedLong = None, + sns_notifications_enabled: Boolean = None, + tag_specifications: TagSpecificationList = None, + **kwargs, + ) -> CreateRouteServerResult: + raise NotImplementedError + + @handler("CreateRouteServerEndpoint") + def create_route_server_endpoint( + self, + context: RequestContext, + route_server_id: RouteServerId, + subnet_id: SubnetId, + client_token: String = None, + dry_run: Boolean = None, + tag_specifications: TagSpecificationList = None, + **kwargs, + ) -> CreateRouteServerEndpointResult: + raise NotImplementedError + + @handler("CreateRouteServerPeer") + def create_route_server_peer( + self, + context: RequestContext, + route_server_endpoint_id: RouteServerEndpointId, + peer_address: String, + bgp_options: RouteServerBgpOptionsRequest, + dry_run: Boolean = None, + tag_specifications: TagSpecificationList = None, + **kwargs, + ) -> CreateRouteServerPeerResult: + raise NotImplementedError + @handler("CreateRouteTable") def create_route_table( self, @@ -22139,6 +22587,36 @@ def delete_route( ) -> None: raise NotImplementedError + @handler("DeleteRouteServer") + def delete_route_server( + self, + context: RequestContext, + route_server_id: RouteServerId, + dry_run: Boolean = None, + **kwargs, + ) -> DeleteRouteServerResult: + raise NotImplementedError + + @handler("DeleteRouteServerEndpoint") + def delete_route_server_endpoint( + self, + context: RequestContext, + route_server_endpoint_id: RouteServerEndpointId, + dry_run: Boolean = None, + **kwargs, + ) -> DeleteRouteServerEndpointResult: + raise NotImplementedError + + @handler("DeleteRouteServerPeer") + def delete_route_server_peer( + self, + context: RequestContext, + route_server_peer_id: RouteServerPeerId, + dry_run: Boolean = None, + **kwargs, + ) -> DeleteRouteServerPeerResult: + raise NotImplementedError + @handler("DeleteRouteTable") def delete_route_table( self, @@ -23844,6 +24322,45 @@ def describe_reserved_instances_offerings( ) -> DescribeReservedInstancesOfferingsResult: raise NotImplementedError + @handler("DescribeRouteServerEndpoints") + def describe_route_server_endpoints( + self, + context: RequestContext, + route_server_endpoint_ids: RouteServerEndpointIdsList = None, + next_token: String = None, + max_results: RouteServerMaxResults = None, + filters: FilterList = None, + dry_run: Boolean = None, + **kwargs, + ) -> DescribeRouteServerEndpointsResult: + raise NotImplementedError + + @handler("DescribeRouteServerPeers") + def describe_route_server_peers( + self, + context: RequestContext, + route_server_peer_ids: RouteServerPeerIdsList = None, + next_token: String = None, + max_results: RouteServerMaxResults = None, + filters: FilterList = None, + dry_run: Boolean = None, + **kwargs, + ) -> DescribeRouteServerPeersResult: + raise NotImplementedError + + @handler("DescribeRouteServers") + def describe_route_servers( + self, + context: RequestContext, + route_server_ids: RouteServerIdsList = None, + next_token: String = None, + max_results: RouteServerMaxResults = None, + filters: FilterList = None, + dry_run: Boolean = None, + **kwargs, + ) -> DescribeRouteServersResult: + raise NotImplementedError + @handler("DescribeRouteTables") def describe_route_tables( self, @@ -24759,6 +25276,17 @@ def disable_ipam_organization_admin_account( ) -> DisableIpamOrganizationAdminAccountResult: raise NotImplementedError + @handler("DisableRouteServerPropagation") + def disable_route_server_propagation( + self, + context: RequestContext, + route_server_id: RouteServerId, + route_table_id: RouteTableId, + dry_run: Boolean = None, + **kwargs, + ) -> DisableRouteServerPropagationResult: + raise NotImplementedError + @handler("DisableSerialConsoleAccess") def disable_serial_console_access( self, context: RequestContext, dry_run: Boolean = None, **kwargs @@ -24895,6 +25423,17 @@ def disassociate_nat_gateway_address( ) -> DisassociateNatGatewayAddressResult: raise NotImplementedError + @handler("DisassociateRouteServer") + def disassociate_route_server( + self, + context: RequestContext, + route_server_id: RouteServerId, + vpc_id: VpcId, + dry_run: Boolean = None, + **kwargs, + ) -> DisassociateRouteServerResult: + raise NotImplementedError + @handler("DisassociateRouteTable") def disassociate_route_table( self, @@ -25092,6 +25631,17 @@ def enable_reachability_analyzer_organization_sharing( ) -> EnableReachabilityAnalyzerOrganizationSharingResult: raise NotImplementedError + @handler("EnableRouteServerPropagation") + def enable_route_server_propagation( + self, + context: RequestContext, + route_server_id: RouteServerId, + route_table_id: RouteTableId, + dry_run: Boolean = None, + **kwargs, + ) -> EnableRouteServerPropagationResult: + raise NotImplementedError + @handler("EnableSerialConsoleAccess") def enable_serial_console_access( self, context: RequestContext, dry_run: Boolean = None, **kwargs @@ -25579,6 +26129,40 @@ def get_reserved_instances_exchange_quote( ) -> GetReservedInstancesExchangeQuoteResult: raise NotImplementedError + @handler("GetRouteServerAssociations") + def get_route_server_associations( + self, + context: RequestContext, + route_server_id: RouteServerId, + dry_run: Boolean = None, + **kwargs, + ) -> GetRouteServerAssociationsResult: + raise NotImplementedError + + @handler("GetRouteServerPropagations") + def get_route_server_propagations( + self, + context: RequestContext, + route_server_id: RouteServerId, + route_table_id: RouteTableId = None, + dry_run: Boolean = None, + **kwargs, + ) -> GetRouteServerPropagationsResult: + raise NotImplementedError + + @handler("GetRouteServerRoutingDatabase") + def get_route_server_routing_database( + self, + context: RequestContext, + route_server_id: RouteServerId, + next_token: String = None, + max_results: RouteServerMaxResults = None, + dry_run: Boolean = None, + filters: FilterList = None, + **kwargs, + ) -> GetRouteServerRoutingDatabaseResult: + raise NotImplementedError + @handler("GetSecurityGroupsForVpc") def get_security_groups_for_vpc( self, @@ -26394,6 +26978,19 @@ def modify_reserved_instances( ) -> ModifyReservedInstancesResult: raise NotImplementedError + @handler("ModifyRouteServer") + def modify_route_server( + self, + context: RequestContext, + route_server_id: RouteServerId, + persist_routes: RouteServerPersistRoutesAction = None, + persist_routes_duration: BoxedLong = None, + sns_notifications_enabled: Boolean = None, + dry_run: Boolean = None, + **kwargs, + ) -> ModifyRouteServerResult: + raise NotImplementedError + @handler("ModifySecurityGroupRules") def modify_security_group_rules( self, diff --git a/localstack-core/localstack/aws/api/events/__init__.py b/localstack-core/localstack/aws/api/events/__init__.py index e1a17b290b1be..fa88310b47693 100644 --- a/localstack-core/localstack/aws/api/events/__init__.py +++ b/localstack-core/localstack/aws/api/events/__init__.py @@ -36,6 +36,7 @@ EndpointUrl = str ErrorCode = str ErrorMessage = str +EventBusArn = str EventBusDescription = str EventBusName = str EventBusNameOrArn = str @@ -329,7 +330,7 @@ class AppSyncParameters(TypedDict, total=False): class Archive(TypedDict, total=False): ArchiveName: Optional[ArchiveName] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[EventBusArn] State: Optional[ArchiveState] StateReason: Optional[ArchiveStateReason] RetentionDays: Optional[RetentionDays] @@ -497,10 +498,11 @@ class CreateApiDestinationResponse(TypedDict, total=False): class CreateArchiveRequest(ServiceRequest): ArchiveName: ArchiveName - EventSourceArn: Arn + EventSourceArn: EventBusArn Description: Optional[ArchiveDescription] EventPattern: Optional[EventPattern] RetentionDays: Optional[RetentionDays] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] class CreateArchiveResponse(TypedDict, total=False): @@ -730,11 +732,12 @@ class DescribeArchiveRequest(ServiceRequest): class DescribeArchiveResponse(TypedDict, total=False): ArchiveArn: Optional[ArchiveArn] ArchiveName: Optional[ArchiveName] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[EventBusArn] Description: Optional[ArchiveDescription] EventPattern: Optional[EventPattern] State: Optional[ArchiveState] StateReason: Optional[ArchiveStateReason] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] RetentionDays: Optional[RetentionDays] SizeBytes: Optional[Long] EventCount: Optional[Long] @@ -836,7 +839,7 @@ class DescribeReplayResponse(TypedDict, total=False): Description: Optional[ReplayDescription] State: Optional[ReplayState] StateReason: Optional[ReplayStateReason] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[ArchiveArn] Destination: Optional[ReplayDestination] EventStartTime: Optional[Timestamp] EventEndTime: Optional[Timestamp] @@ -994,7 +997,7 @@ class ListApiDestinationsResponse(TypedDict, total=False): class ListArchivesRequest(ServiceRequest): NamePrefix: Optional[ArchiveName] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[EventBusArn] State: Optional[ArchiveState] NextToken: Optional[NextToken] Limit: Optional[LimitMax100] @@ -1094,14 +1097,14 @@ class ListPartnerEventSourcesResponse(TypedDict, total=False): class ListReplaysRequest(ServiceRequest): NamePrefix: Optional[ReplayName] State: Optional[ReplayState] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[ArchiveArn] NextToken: Optional[NextToken] Limit: Optional[LimitMax100] class Replay(TypedDict, total=False): ReplayName: Optional[ReplayName] - EventSourceArn: Optional[Arn] + EventSourceArn: Optional[ArchiveArn] State: Optional[ReplayState] StateReason: Optional[ReplayStateReason] EventStartTime: Optional[Timestamp] @@ -1391,7 +1394,7 @@ class RemoveTargetsResponse(TypedDict, total=False): class StartReplayRequest(ServiceRequest): ReplayName: ReplayName Description: Optional[ReplayDescription] - EventSourceArn: Arn + EventSourceArn: ArchiveArn EventStartTime: Timestamp EventEndTime: Timestamp Destination: ReplayDestination @@ -1455,6 +1458,7 @@ class UpdateArchiveRequest(ServiceRequest): Description: Optional[ArchiveDescription] EventPattern: Optional[EventPattern] RetentionDays: Optional[RetentionDays] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] class UpdateArchiveResponse(TypedDict, total=False): @@ -1581,10 +1585,11 @@ def create_archive( self, context: RequestContext, archive_name: ArchiveName, - event_source_arn: Arn, + event_source_arn: EventBusArn, description: ArchiveDescription = None, event_pattern: EventPattern = None, retention_days: RetentionDays = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> CreateArchiveResponse: raise NotImplementedError @@ -1788,7 +1793,7 @@ def list_archives( self, context: RequestContext, name_prefix: ArchiveName = None, - event_source_arn: Arn = None, + event_source_arn: EventBusArn = None, state: ArchiveState = None, next_token: NextToken = None, limit: LimitMax100 = None, @@ -1870,7 +1875,7 @@ def list_replays( context: RequestContext, name_prefix: ReplayName = None, state: ReplayState = None, - event_source_arn: Arn = None, + event_source_arn: ArchiveArn = None, next_token: NextToken = None, limit: LimitMax100 = None, **kwargs, @@ -2004,7 +2009,7 @@ def start_replay( self, context: RequestContext, replay_name: ReplayName, - event_source_arn: Arn, + event_source_arn: ArchiveArn, event_start_time: Timestamp, event_end_time: Timestamp, destination: ReplayDestination, @@ -2053,6 +2058,7 @@ def update_archive( description: ArchiveDescription = None, event_pattern: EventPattern = None, retention_days: RetentionDays = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> UpdateArchiveResponse: raise NotImplementedError diff --git a/localstack-core/localstack/aws/api/route53/__init__.py b/localstack-core/localstack/aws/api/route53/__init__.py index 820b700b5ef45..74a5da6e5a1ef 100644 --- a/localstack-core/localstack/aws/api/route53/__init__.py +++ b/localstack-core/localstack/aws/api/route53/__init__.py @@ -277,6 +277,8 @@ class ResourceRecordSetRegion(StrEnum): ap_southeast_5 = "ap-southeast-5" mx_central_1 = "mx-central-1" ap_southeast_7 = "ap-southeast-7" + us_gov_east_1 = "us-gov-east-1" + us_gov_west_1 = "us-gov-west-1" class ReusableDelegationSetLimitType(StrEnum): diff --git a/localstack-core/localstack/aws/api/s3control/__init__.py b/localstack-core/localstack/aws/api/s3control/__init__.py index a8a5963c4cbfd..ff20040184f01 100644 --- a/localstack-core/localstack/aws/api/s3control/__init__.py +++ b/localstack-core/localstack/aws/api/s3control/__init__.py @@ -405,6 +405,17 @@ class S3StorageClass(StrEnum): GLACIER_IR = "GLACIER_IR" +class ScopePermission(StrEnum): + GetObject = "GetObject" + GetObjectAttributes = "GetObjectAttributes" + ListMultipartUploadParts = "ListMultipartUploadParts" + ListBucket = "ListBucket" + ListBucketMultipartUploads = "ListBucketMultipartUploads" + PutObject = "PutObject" + DeleteObject = "DeleteObject" + AbortMultipartUpload = "AbortMultipartUpload" + + class SseKmsEncryptedObjectsStatus(StrEnum): Enabled = "Enabled" Disabled = "Disabled" @@ -824,6 +835,15 @@ class CreateAccessPointForObjectLambdaResult(TypedDict, total=False): Alias: Optional[ObjectLambdaAccessPointAlias] +ScopePermissionList = List[ScopePermission] +PrefixesList = List[Prefix] + + +class Scope(TypedDict, total=False): + Prefixes: Optional[PrefixesList] + Permissions: Optional[ScopePermissionList] + + class CreateAccessPointRequest(ServiceRequest): AccountId: AccountId Name: AccessPointName @@ -831,6 +851,7 @@ class CreateAccessPointRequest(ServiceRequest): VpcConfiguration: Optional[VpcConfiguration] PublicAccessBlockConfiguration: Optional[PublicAccessBlockConfiguration] BucketAccountId: Optional[AccountId] + Scope: Optional[Scope] class CreateAccessPointResult(TypedDict, total=False): @@ -1222,6 +1243,11 @@ class DeleteAccessPointRequest(ServiceRequest): Name: AccessPointName +class DeleteAccessPointScopeRequest(ServiceRequest): + AccountId: AccountId + Name: AccessPointName + + class DeleteBucketLifecycleConfigurationRequest(ServiceRequest): AccountId: AccountId Bucket: BucketName @@ -1561,6 +1587,15 @@ class GetAccessPointResult(TypedDict, total=False): BucketAccountId: Optional[AccountId] +class GetAccessPointScopeRequest(ServiceRequest): + AccountId: AccountId + Name: AccessPointName + + +class GetAccessPointScopeResult(TypedDict, total=False): + Scope: Optional[Scope] + + class GetBucketLifecycleConfigurationRequest(ServiceRequest): AccountId: AccountId Bucket: BucketName @@ -1965,6 +2000,18 @@ class ListAccessGrantsResult(TypedDict, total=False): AccessGrantsList: Optional[AccessGrantsList] +class ListAccessPointsForDirectoryBucketsRequest(ServiceRequest): + AccountId: AccountId + DirectoryBucket: Optional[BucketName] + NextToken: Optional[NonEmptyMaxLength1024String] + MaxResults: Optional[MaxResults] + + +class ListAccessPointsForDirectoryBucketsResult(TypedDict, total=False): + AccessPointList: Optional[AccessPointList] + NextToken: Optional[NonEmptyMaxLength1024String] + + class ListAccessPointsForObjectLambdaRequest(ServiceRequest): AccountId: AccountId NextToken: Optional[NonEmptyMaxLength1024String] @@ -2137,6 +2184,12 @@ class PutAccessPointPolicyRequest(ServiceRequest): Policy: Policy +class PutAccessPointScopeRequest(ServiceRequest): + AccountId: AccountId + Name: AccessPointName + Scope: Scope + + class PutBucketLifecycleConfigurationRequest(ServiceRequest): AccountId: AccountId Bucket: BucketName @@ -2360,6 +2413,7 @@ def create_access_point( vpc_configuration: VpcConfiguration = None, public_access_block_configuration: PublicAccessBlockConfiguration = None, bucket_account_id: AccountId = None, + scope: Scope = None, **kwargs, ) -> CreateAccessPointResult: raise NotImplementedError @@ -2498,6 +2552,12 @@ def delete_access_point_policy_for_object_lambda( ) -> None: raise NotImplementedError + @handler("DeleteAccessPointScope") + def delete_access_point_scope( + self, context: RequestContext, account_id: AccountId, name: AccessPointName, **kwargs + ) -> None: + raise NotImplementedError + @handler("DeleteBucket") def delete_bucket( self, context: RequestContext, account_id: AccountId, bucket: BucketName, **kwargs @@ -2687,6 +2747,12 @@ def get_access_point_policy_status_for_object_lambda( ) -> GetAccessPointPolicyStatusForObjectLambdaResult: raise NotImplementedError + @handler("GetAccessPointScope") + def get_access_point_scope( + self, context: RequestContext, account_id: AccountId, name: AccessPointName, **kwargs + ) -> GetAccessPointScopeResult: + raise NotImplementedError + @handler("GetBucket") def get_bucket( self, context: RequestContext, account_id: AccountId, bucket: BucketName, **kwargs @@ -2858,6 +2924,18 @@ def list_access_points( ) -> ListAccessPointsResult: raise NotImplementedError + @handler("ListAccessPointsForDirectoryBuckets") + def list_access_points_for_directory_buckets( + self, + context: RequestContext, + account_id: AccountId, + directory_bucket: BucketName = None, + next_token: NonEmptyMaxLength1024String = None, + max_results: MaxResults = None, + **kwargs, + ) -> ListAccessPointsForDirectoryBucketsResult: + raise NotImplementedError + @handler("ListAccessPointsForObjectLambda") def list_access_points_for_object_lambda( self, @@ -2987,6 +3065,17 @@ def put_access_point_policy_for_object_lambda( ) -> None: raise NotImplementedError + @handler("PutAccessPointScope") + def put_access_point_scope( + self, + context: RequestContext, + account_id: AccountId, + name: AccessPointName, + scope: Scope, + **kwargs, + ) -> None: + raise NotImplementedError + @handler("PutBucketLifecycleConfiguration") def put_bucket_lifecycle_configuration( self, diff --git a/localstack-core/localstack/aws/api/transcribe/__init__.py b/localstack-core/localstack/aws/api/transcribe/__init__.py index 2ab6b49a74b37..440363a46dd95 100644 --- a/localstack-core/localstack/aws/api/transcribe/__init__.py +++ b/localstack-core/localstack/aws/api/transcribe/__init__.py @@ -177,6 +177,7 @@ class LanguageCode(StrEnum): uk_UA = "uk-UA" uz_UZ = "uz-UZ" wo_SN = "wo-SN" + zh_HK = "zh-HK" zu_ZA = "zu-ZA" diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index a51fd805288e5..cdb6e3ad32904 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -44,6 +44,7 @@ DescribeReplayResponse, DescribeRuleResponse, EndpointId, + EventBusArn, EventBusDescription, EventBusList, EventBusName, @@ -921,12 +922,14 @@ def create_archive( self, context: RequestContext, archive_name: ArchiveName, - event_source_arn: Arn, + event_source_arn: EventBusArn, description: ArchiveDescription = None, event_pattern: EventPattern = None, retention_days: RetentionDays = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> CreateArchiveResponse: + # TODO add support for kms_key_identifier region = context.region account_id = context.account_id store = self.get_store(region, account_id) @@ -1022,8 +1025,10 @@ def update_archive( description: ArchiveDescription = None, event_pattern: EventPattern = None, retention_days: RetentionDays = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> UpdateArchiveResponse: + # TODO add support for kms_key_identifier region = context.region account_id = context.account_id store = self.get_store(region, account_id) diff --git a/pyproject.toml b/pyproject.toml index 45e6ae9921193..de157572a9482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.37.23", + "boto3==1.37.28", # pinned / updated by ASF update action - "botocore==1.37.23", + "botocore==1.37.28", "awscrt>=0.13.14", "cbor2>=5.5.0", "dnspython>=1.16.0", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index a1dea158e97d1..558317b4f8682 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -11,9 +11,9 @@ attrs==25.3.0 # referencing awscrt==0.25.4 # via localstack-core (pyproject.toml) -boto3==1.37.23 +boto3==1.37.28 # via localstack-core (pyproject.toml) -botocore==1.37.23 +botocore==1.37.28 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index fdb7ffde053b3..5bedafe316a1f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.23 +awscli==1.38.28 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.23 +boto3==1.37.28 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.23 +botocore==1.37.28 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 78134753dcea2..b412a5ed50bc8 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -29,17 +29,17 @@ aws-sam-translator==1.95.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.23 +awscli==1.38.28 # via localstack-core (pyproject.toml) awscrt==0.25.4 # via localstack-core -boto3==1.37.23 +boto3==1.37.28 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.23 +botocore==1.37.28 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index ba2f1a0027a6a..7fa232a0d3acb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.23 +awscli==1.38.28 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.23 +boto3==1.37.28 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.23 +botocore==1.37.28 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index ed997c151c141..898d1f583dc47 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -41,11 +41,11 @@ aws-sam-translator==1.95.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.23 +awscli==1.38.28 # via localstack-core awscrt==0.25.4 # via localstack-core -boto3==1.37.23 +boto3==1.37.28 # via # amazon-kclpy # aws-sam-translator @@ -53,7 +53,7 @@ boto3==1.37.23 # moto-ext boto3-stubs==1.37.24 # via localstack-core (pyproject.toml) -botocore==1.37.23 +botocore==1.37.28 # via # aws-xray-sdk # awscli From e7045a420c30d5c88f6aadd0b42d8688786e102f Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:03:23 +0200 Subject: [PATCH 029/108] Upgrade pinned Python dependencies (#12498) Co-authored-by: LocalStack Bot --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 4 +-- requirements-dev.txt | 24 ++++++++-------- requirements-runtime.txt | 14 ++++----- requirements-test.txt | 22 +++++++------- requirements-typehint.txt | 54 +++++++++++++++++------------------ 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9e87ed90b64a..45f11e9774cf8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.2 + rev: v0.11.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 558317b4f8682..db527687b8e99 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -9,7 +9,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -awscrt==0.25.4 +awscrt==0.25.7 # via localstack-core (pyproject.toml) boto3==1.37.28 # via localstack-core (pyproject.toml) @@ -180,7 +180,7 @@ six==1.17.0 # rfc3339-validator tailer==0.4.1 # via localstack-core (pyproject.toml) -typing-extensions==4.13.0 +typing-extensions==4.13.1 # via # localstack-twisted # pyopenssl diff --git a/requirements-dev.txt b/requirements-dev.txt index 5bedafe316a1f..838b1c18ecd40 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.230 +aws-cdk-asset-awscli-v1==2.2.231 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.187.0 +aws-cdk-lib==2.188.0 # via localstack-core -aws-sam-translator==1.95.0 +aws-sam-translator==1.96.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.4 +awscrt==0.25.7 # via localstack-core boto3==1.37.28 # via @@ -82,7 +82,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.1 +cfn-lint==1.32.4 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -197,14 +197,14 @@ joserfc==1.0.4 # via moto-ext jpype1-ext==0.0.2 # via localstack-core -jsii==1.110.0 +jsii==1.111.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-cloud-assembly-schema # aws-cdk-lib # constructs -json5==0.10.0 +json5==0.12.0 # via localstack-core jsonpatch==1.33 # via @@ -275,7 +275,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.0 +orderly-set==5.3.1 # via deepdiff packaging==24.2 # via @@ -331,9 +331,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.1 +pydantic==2.11.2 # via aws-sam-translator -pydantic-core==2.33.0 +pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich @@ -425,7 +425,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.11.2 +ruff==0.11.4 # via localstack-core (pyproject.toml) s3transfer==0.11.4 # via @@ -457,7 +457,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.13.0 +typing-extensions==4.13.1 # via # anyio # aws-sam-translator diff --git a/requirements-runtime.txt b/requirements-runtime.txt index b412a5ed50bc8..c54b4a64f9b86 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -23,7 +23,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-sam-translator==1.95.0 +aws-sam-translator==1.96.0 # via # cfn-lint # localstack-core (pyproject.toml) @@ -31,7 +31,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core (pyproject.toml) -awscrt==0.25.4 +awscrt==0.25.7 # via localstack-core boto3==1.37.28 # via @@ -64,7 +64,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.1 +cfn-lint==1.32.4 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -145,7 +145,7 @@ joserfc==1.0.4 # via moto-ext jpype1-ext==0.0.2 # via localstack-core (pyproject.toml) -json5==0.10.0 +json5==0.12.0 # via localstack-core (pyproject.toml) jsonpatch==1.33 # via @@ -239,9 +239,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.1 +pydantic==2.11.2 # via aws-sam-translator -pydantic-core==2.33.0 +pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich @@ -332,7 +332,7 @@ tailer==0.4.1 # via # localstack-core # localstack-core (pyproject.toml) -typing-extensions==4.13.0 +typing-extensions==4.13.1 # via # aws-sam-translator # cfn-lint diff --git a/requirements-test.txt b/requirements-test.txt index 7fa232a0d3acb..24d188ffd3a25 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.230 +aws-cdk-asset-awscli-v1==2.2.231 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.187.0 +aws-cdk-lib==2.188.0 # via localstack-core (pyproject.toml) -aws-sam-translator==1.95.0 +aws-sam-translator==1.96.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.4 +awscrt==0.25.7 # via localstack-core boto3==1.37.28 # via @@ -80,7 +80,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.1 +cfn-lint==1.32.4 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -181,14 +181,14 @@ joserfc==1.0.4 # via moto-ext jpype1-ext==0.0.2 # via localstack-core -jsii==1.110.0 +jsii==1.111.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-cloud-assembly-schema # aws-cdk-lib # constructs -json5==0.10.0 +json5==0.12.0 # via localstack-core jsonpatch==1.33 # via @@ -254,7 +254,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.0 +orderly-set==5.3.1 # via deepdiff packaging==24.2 # via @@ -301,9 +301,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.1 +pydantic==2.11.2 # via aws-sam-translator -pydantic-core==2.33.0 +pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich @@ -419,7 +419,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.13.0 +typing-extensions==4.13.1 # via # anyio # aws-sam-translator diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 898d1f583dc47..47dae59c985dd 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.230 +aws-cdk-asset-awscli-v1==2.2.231 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.187.0 +aws-cdk-lib==2.188.0 # via localstack-core -aws-sam-translator==1.95.0 +aws-sam-translator==1.96.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.4 +awscrt==0.25.7 # via localstack-core boto3==1.37.28 # via @@ -51,7 +51,7 @@ boto3==1.37.28 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.37.24 +boto3-stubs==1.37.29 # via localstack-core (pyproject.toml) botocore==1.37.28 # via @@ -61,7 +61,7 @@ botocore==1.37.28 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.37.24 +botocore-stubs==1.37.29 # via boto3-stubs build==1.2.2.post1 # via @@ -86,7 +86,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.1 +cfn-lint==1.32.4 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -201,14 +201,14 @@ joserfc==1.0.4 # via moto-ext jpype1-ext==0.0.2 # via localstack-core -jsii==1.110.0 +jsii==1.111.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-cloud-assembly-schema # aws-cdk-lib # constructs -json5==0.10.0 +json5==0.12.0 # via localstack-core jsonpatch==1.33 # via @@ -298,7 +298,7 @@ mypy-boto3-cloudtrail==1.37.8 # via boto3-stubs mypy-boto3-cloudwatch==1.37.0 # via boto3-stubs -mypy-boto3-codebuild==1.37.23 +mypy-boto3-codebuild==1.37.29 # via boto3-stubs mypy-boto3-codecommit==1.37.0 # via boto3-stubs @@ -322,11 +322,11 @@ mypy-boto3-dynamodb==1.37.12 # via boto3-stubs mypy-boto3-dynamodbstreams==1.37.0 # via boto3-stubs -mypy-boto3-ec2==1.37.24 +mypy-boto3-ec2==1.37.28 # via boto3-stubs -mypy-boto3-ecr==1.37.11 +mypy-boto3-ecr==1.37.26 # via boto3-stubs -mypy-boto3-ecs==1.37.23 +mypy-boto3-ecs==1.37.26 # via boto3-stubs mypy-boto3-efs==1.37.0 # via boto3-stubs @@ -344,7 +344,7 @@ mypy-boto3-emr-serverless==1.37.0 # via boto3-stubs mypy-boto3-es==1.37.0 # via boto3-stubs -mypy-boto3-events==1.37.0 +mypy-boto3-events==1.37.28 # via boto3-stubs mypy-boto3-firehose==1.37.0 # via boto3-stubs @@ -352,7 +352,7 @@ mypy-boto3-fis==1.37.0 # via boto3-stubs mypy-boto3-glacier==1.37.0 # via boto3-stubs -mypy-boto3-glue==1.37.13 +mypy-boto3-glue==1.37.29 # via boto3-stubs mypy-boto3-iam==1.37.22 # via boto3-stubs @@ -394,7 +394,7 @@ mypy-boto3-mwaa==1.37.0 # via boto3-stubs mypy-boto3-neptune==1.37.0 # via boto3-stubs -mypy-boto3-opensearch==1.37.0 +mypy-boto3-opensearch==1.37.27 # via boto3-stubs mypy-boto3-organizations==1.37.0 # via boto3-stubs @@ -420,15 +420,15 @@ mypy-boto3-resource-groups==1.37.0 # via boto3-stubs mypy-boto3-resourcegroupstaggingapi==1.37.0 # via boto3-stubs -mypy-boto3-route53==1.37.15 +mypy-boto3-route53==1.37.27 # via boto3-stubs mypy-boto3-route53resolver==1.37.0 # via boto3-stubs mypy-boto3-s3==1.37.24 # via boto3-stubs -mypy-boto3-s3control==1.37.24 +mypy-boto3-s3control==1.37.28 # via boto3-stubs -mypy-boto3-sagemaker==1.37.23 +mypy-boto3-sagemaker==1.37.27 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.37.0 # via boto3-stubs @@ -440,7 +440,7 @@ mypy-boto3-servicediscovery==1.37.0 # via boto3-stubs mypy-boto3-ses==1.37.0 # via boto3-stubs -mypy-boto3-sesv2==1.37.24 +mypy-boto3-sesv2==1.37.27 # via boto3-stubs mypy-boto3-sns==1.37.0 # via boto3-stubs @@ -458,7 +458,7 @@ mypy-boto3-timestream-query==1.37.0 # via boto3-stubs mypy-boto3-timestream-write==1.37.0 # via boto3-stubs -mypy-boto3-transcribe==1.37.5 +mypy-boto3-transcribe==1.37.27 # via boto3-stubs mypy-boto3-verifiedpermissions==1.37.0 # via boto3-stubs @@ -485,7 +485,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.0 +orderly-set==5.3.1 # via deepdiff packaging==24.2 # via @@ -541,9 +541,9 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.1 +pydantic==2.11.2 # via aws-sam-translator -pydantic-core==2.33.0 +pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich @@ -635,7 +635,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.11.2 +ruff==0.11.4 # via localstack-core s3transfer==0.11.4 # via @@ -667,11 +667,11 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -types-awscrt==0.24.2 +types-awscrt==0.25.7 # via botocore-stubs types-s3transfer==0.11.4 # via boto3-stubs -typing-extensions==4.13.0 +typing-extensions==4.13.1 # via # anyio # aws-sam-translator From 7189fffbc88319ee647f4a1af9dc9d054b6abb43 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Tue, 8 Apr 2025 14:43:31 +0530 Subject: [PATCH 030/108] Bump moto-ext to 5.1.1.post2 (#12484) --- pyproject.toml | 2 +- requirements-dev.txt | 2 +- requirements-runtime.txt | 2 +- requirements-test.txt | 2 +- requirements-typehint.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de157572a9482..94eb3a2e67cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.1.1.post1", + "moto-ext[all]==5.1.1.post2", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 838b1c18ecd40..011b12d670d12 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -250,7 +250,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post1 +moto-ext==5.1.1.post2 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-runtime.txt b/requirements-runtime.txt index c54b4a64f9b86..40fcaf5d0a44d 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -188,7 +188,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post1 +moto-ext==5.1.1.post2 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy diff --git a/requirements-test.txt b/requirements-test.txt index 24d188ffd3a25..27acf9dd37d94 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -234,7 +234,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post1 +moto-ext==5.1.1.post2 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 47dae59c985dd..5c4bd196e2c46 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -254,7 +254,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post1 +moto-ext==5.1.1.post2 # via localstack-core mpmath==1.3.0 # via sympy From 7f920e3fc5882e38a007f8a15825b27ed658b56b Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:31:34 +0200 Subject: [PATCH 031/108] Scheduled GitHub Action to generate artifacts with feature catalog files (#12501) --- .../create_artifact_with_features_files.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/create_artifact_with_features_files.yml diff --git a/.github/workflows/create_artifact_with_features_files.yml b/.github/workflows/create_artifact_with_features_files.yml new file mode 100644 index 0000000000000..30e87074a19c0 --- /dev/null +++ b/.github/workflows/create_artifact_with_features_files.yml @@ -0,0 +1,14 @@ +name: AWS / Archive feature files + +on: + schedule: + - cron: 0 9 * * TUE + workflow_dispatch: + +jobs: + validate-features-files: + name: Create artifact with features files + uses: localstack/meta/.github/workflows/create-artifact-with-features-files.yml@main + with: + artifact_name: 'features-files' + aws_services_path: 'localstack-core/localstack/services' From 97bc2c77eacdfb2f2ac8e8a2db5b512a526d4054 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Tue, 8 Apr 2025 14:40:07 +0200 Subject: [PATCH 032/108] Add new volume dir mount option, rename VolumeBind to BindMount (#12471) --- .../localstack/dev/run/configurators.py | 22 +++--- .../invocation/docker_runtime_executor.py | 4 +- .../localstack/utils/analytics/metadata.py | 4 +- localstack-core/localstack/utils/bootstrap.py | 11 +-- .../utils/container_utils/container_client.py | 75 +++++++++++++++---- .../container_utils/docker_cmd_client.py | 7 +- .../bootstrap/test_container_configurators.py | 4 +- tests/bootstrap/test_init.py | 4 +- tests/unit/test_docker_utils.py | 30 +++++++- tests/unit/utils/test_bootstrap.py | 6 +- 10 files changed, 123 insertions(+), 44 deletions(-) diff --git a/localstack-core/localstack/dev/run/configurators.py b/localstack-core/localstack/dev/run/configurators.py index 2c3b253965e87..4f1b9e3e29cde 100644 --- a/localstack-core/localstack/dev/run/configurators.py +++ b/localstack-core/localstack/dev/run/configurators.py @@ -10,9 +10,9 @@ from localstack import config, constants from localstack.utils.bootstrap import ContainerConfigurators from localstack.utils.container_utils.container_client import ( + BindMount, ContainerClient, ContainerConfiguration, - VolumeBind, VolumeMappings, ) from localstack.utils.docker_utils import DOCKER_CLIENT @@ -107,7 +107,7 @@ def __call__(self, cfg: ContainerConfiguration): # encoding needs to be "utf-8" since scripts could include emojis file.write_text(self.script, newline="\n", encoding="utf-8") file.chmod(0o777) - cfg.volumes.add(VolumeBind(str(file), f"/tmp/{file.name}")) + cfg.volumes.add(BindMount(str(file), f"/tmp/{file.name}")) cfg.entrypoint = f"/tmp/{file.name}" @@ -137,7 +137,7 @@ def __call__(self, cfg: ContainerConfiguration): cfg.volumes.add( # read_only=False is a temporary workaround to make the mounting of the pro source work # this can be reverted once we don't need the nested mounting anymore - VolumeBind(str(source), self.container_paths.localstack_source_dir, read_only=False) + BindMount(str(source), self.container_paths.localstack_source_dir, read_only=False) ) # ext source code if available @@ -145,7 +145,7 @@ def __call__(self, cfg: ContainerConfiguration): source = self.host_paths.aws_pro_package_dir if source.exists(): cfg.volumes.add( - VolumeBind( + BindMount( str(source), self.container_paths.localstack_pro_source_dir, read_only=True ) ) @@ -163,7 +163,7 @@ def __call__(self, cfg: ContainerConfiguration): source = self.host_paths.localstack_project_dir / "bin" / "docker-entrypoint.sh" if source.exists(): cfg.volumes.add( - VolumeBind(str(source), self.container_paths.docker_entrypoint, read_only=True) + BindMount(str(source), self.container_paths.docker_entrypoint, read_only=True) ) def try_mount_to_site_packages(self, cfg: ContainerConfiguration, sources_path: Path): @@ -177,7 +177,7 @@ def try_mount_to_site_packages(self, cfg: ContainerConfiguration, sources_path: """ if sources_path.exists(): cfg.volumes.add( - VolumeBind( + BindMount( str(sources_path), self.container_paths.dependency_source(sources_path.name), read_only=True, @@ -219,7 +219,7 @@ def __call__(self, cfg: ContainerConfiguration): host_path = self.host_paths.aws_community_package_dir if host_path.exists(): cfg.volumes.append( - VolumeBind( + BindMount( str(host_path), self.localstack_community_entry_points, read_only=True ) ) @@ -244,7 +244,7 @@ def __call__(self, cfg: ContainerConfiguration): ) if host_path.is_file(): cfg.volumes.add( - VolumeBind( + BindMount( str(host_path), str(container_path), read_only=True, @@ -260,7 +260,7 @@ def __call__(self, cfg: ContainerConfiguration): ) if host_path.is_file(): cfg.volumes.add( - VolumeBind( + BindMount( str(host_path), str(container_path), read_only=True, @@ -270,7 +270,7 @@ def __call__(self, cfg: ContainerConfiguration): for host_path in self.host_paths.workspace_dir.glob( f"*/{dep}.egg-info/entry_points.txt" ): - cfg.volumes.add(VolumeBind(str(host_path), str(container_path), read_only=True)) + cfg.volumes.add(BindMount(str(host_path), str(container_path), read_only=True)) break @@ -330,7 +330,7 @@ def __call__(self, cfg: ContainerConfiguration): if self._has_mount(cfg.volumes, target_path): continue - cfg.volumes.append(VolumeBind(str(dep_path), target_path)) + cfg.volumes.append(BindMount(str(dep_path), target_path)) def _can_be_source_path(self, path: Path) -> bool: return path.is_dir() or (path.name.endswith(".py") and not path.name.startswith("__")) diff --git a/localstack-core/localstack/services/lambda_/invocation/docker_runtime_executor.py b/localstack-core/localstack/services/lambda_/invocation/docker_runtime_executor.py index ec9e20ef46d33..c67f39addb414 100644 --- a/localstack-core/localstack/services/lambda_/invocation/docker_runtime_executor.py +++ b/localstack-core/localstack/services/lambda_/invocation/docker_runtime_executor.py @@ -32,13 +32,13 @@ from localstack.services.lambda_.runtimes import IMAGE_MAPPING from localstack.utils.container_networking import get_main_container_name from localstack.utils.container_utils.container_client import ( + BindMount, ContainerConfiguration, DockerNotAvailable, DockerPlatform, NoSuchContainer, NoSuchImage, PortMappings, - VolumeBind, VolumeMappings, ) from localstack.utils.docker_utils import DOCKER_CLIENT as CONTAINER_CLIENT @@ -331,7 +331,7 @@ def start(self, env_vars: dict[str, str]) -> None: if container_config.volumes is None: container_config.volumes = VolumeMappings() container_config.volumes.add( - VolumeBind( + BindMount( str(self.function_version.config.code.get_unzipped_code_location()), "/var/task", read_only=True, diff --git a/localstack-core/localstack/utils/analytics/metadata.py b/localstack-core/localstack/utils/analytics/metadata.py index c0ef292d69121..da135c861a323 100644 --- a/localstack-core/localstack/utils/analytics/metadata.py +++ b/localstack-core/localstack/utils/analytics/metadata.py @@ -237,11 +237,11 @@ def prepare_host_machine_id(): @hooks.configure_localstack_container() def _mount_machine_file(container: Container): - from localstack.utils.container_utils.container_client import VolumeBind + from localstack.utils.container_utils.container_client import BindMount # mount tha machine file from the host's CLI cache directory into the appropriate location in the # container machine_file = os.path.join(config.dirs.cache, "machine.json") if os.path.isfile(machine_file): target = os.path.join(config.dirs.for_container().cache, "machine.json") - container.config.volumes.add(VolumeBind(machine_file, target, read_only=True)) + container.config.volumes.add(BindMount(machine_file, target, read_only=True)) diff --git a/localstack-core/localstack/utils/bootstrap.py b/localstack-core/localstack/utils/bootstrap.py index ddca686698185..e767c22f90b30 100644 --- a/localstack-core/localstack/utils/bootstrap.py +++ b/localstack-core/localstack/utils/bootstrap.py @@ -24,6 +24,7 @@ from localstack.runtime import hooks from localstack.utils.container_networking import get_main_container_name from localstack.utils.container_utils.container_client import ( + BindMount, CancellableStream, ContainerClient, ContainerConfiguration, @@ -33,7 +34,7 @@ NoSuchImage, NoSuchNetwork, PortMappings, - VolumeBind, + VolumeDirMount, VolumeMappings, ) from localstack.utils.container_utils.docker_cmd_client import CmdDockerClient @@ -491,7 +492,7 @@ def mount_docker_socket(cfg: ContainerConfiguration): target = "/var/run/docker.sock" if cfg.volumes.find_target_mapping(target): return - cfg.volumes.add(VolumeBind(source, target)) + cfg.volumes.add(BindMount(source, target)) cfg.env_vars["DOCKER_HOST"] = f"unix://{target}" @staticmethod @@ -501,7 +502,7 @@ def mount_localstack_volume(host_path: str | os.PathLike = None): def _cfg(cfg: ContainerConfiguration): if cfg.volumes.find_target_mapping(constants.DEFAULT_VOLUME_DIR): return - cfg.volumes.add(VolumeBind(str(host_path), constants.DEFAULT_VOLUME_DIR)) + cfg.volumes.add(BindMount(str(host_path), constants.DEFAULT_VOLUME_DIR)) return _cfg @@ -679,7 +680,7 @@ def _cfg(cfg: ContainerConfiguration): return _cfg @staticmethod - def volume(volume: VolumeBind): + def volume(volume: BindMount | VolumeDirMount): def _cfg(cfg: ContainerConfiguration): cfg.volumes.add(volume) @@ -807,7 +808,7 @@ def volume_cli_params(params: Iterable[str] = None): def _cfg(cfg: ContainerConfiguration): for param in params: - cfg.volumes.append(VolumeBind.parse(param)) + cfg.volumes.append(BindMount.parse(param)) return _cfg diff --git a/localstack-core/localstack/utils/container_utils/container_client.py b/localstack-core/localstack/utils/container_utils/container_client.py index 945832829203e..e05fdd6da5a55 100644 --- a/localstack-core/localstack/utils/container_utils/container_client.py +++ b/localstack-core/localstack/utils/container_utils/container_client.py @@ -27,6 +27,7 @@ import dotenv from localstack import config +from localstack.constants import DEFAULT_VOLUME_DIR from localstack.utils.collections import HashableList, ensure_list from localstack.utils.files import TMP_FILES, chmod_r, rm_rf, save_file from localstack.utils.no_exit_argument_parser import NoExitArgumentParser @@ -370,7 +371,7 @@ def __repr__(self): @dataclasses.dataclass -class VolumeBind: +class BindMount: """Represents a --volume argument run/create command. When using VolumeBind to bind-mount a file or directory that does not yet exist on the Docker host, -v creates the endpoint for you. It is always created as a directory. """ @@ -395,8 +396,14 @@ def to_str(self) -> str: return ":".join(args) + def to_docker_sdk_parameters(self) -> tuple[str, dict[str, str]]: + return str(self.host_dir), { + "bind": self.container_dir, + "mode": "ro" if self.read_only else "rw", + } + @classmethod - def parse(cls, param: str) -> "VolumeBind": + def parse(cls, param: str) -> "BindMount": parts = param.split(":") if 1 > len(parts) > 3: raise ValueError(f"Cannot parse volume bind {param}") @@ -408,27 +415,66 @@ def parse(cls, param: str) -> "VolumeBind": return volume +@dataclasses.dataclass +class VolumeDirMount: + volume_path: str + """ + Absolute path inside /var/lib/localstack to mount into the container + """ + container_path: str + """ + Target path inside the started container + """ + read_only: bool = False + + def to_str(self) -> str: + self._validate() + from localstack.utils.docker_utils import get_host_path_for_path_in_docker + + host_dir = get_host_path_for_path_in_docker(self.volume_path) + return f"{host_dir}:{self.container_path}{':ro' if self.read_only else ''}" + + def _validate(self): + if not self.volume_path: + raise ValueError("no volume dir specified") + if config.is_in_docker and not self.volume_path.startswith(DEFAULT_VOLUME_DIR): + raise ValueError(f"volume dir not starting with {DEFAULT_VOLUME_DIR}") + if not self.container_path: + raise ValueError("no container dir specified") + + def to_docker_sdk_parameters(self) -> tuple[str, dict[str, str]]: + self._validate() + from localstack.utils.docker_utils import get_host_path_for_path_in_docker + + host_dir = get_host_path_for_path_in_docker(self.volume_path) + return host_dir, { + "bind": self.container_path, + "mode": "ro" if self.read_only else "rw", + } + + class VolumeMappings: - mappings: List[Union[SimpleVolumeBind, VolumeBind]] + mappings: List[Union[SimpleVolumeBind, BindMount]] - def __init__(self, mappings: List[Union[SimpleVolumeBind, VolumeBind]] = None): + def __init__(self, mappings: List[Union[SimpleVolumeBind, BindMount, VolumeDirMount]] = None): self.mappings = mappings if mappings is not None else [] - def add(self, mapping: Union[SimpleVolumeBind, VolumeBind]): + def add(self, mapping: Union[SimpleVolumeBind, BindMount, VolumeDirMount]): self.append(mapping) def append( self, mapping: Union[ SimpleVolumeBind, - VolumeBind, + BindMount, + VolumeDirMount, ], ): self.mappings.append(mapping) def find_target_mapping( self, container_dir: str - ) -> Optional[Union[SimpleVolumeBind, VolumeBind]]: + ) -> Optional[Union[SimpleVolumeBind, BindMount, VolumeDirMount]]: """ Looks through the volumes and returns the one where the container dir matches ``container_dir``. Returns None if there is no volume mapping to the given container directory. @@ -448,6 +494,12 @@ def __iter__(self): def __repr__(self): return self.mappings.__repr__() + def __len__(self): + return len(self.mappings) + + def __getitem__(self, item: int): + return self.mappings[item] + VolumeType = Literal["bind", "volume"] @@ -1441,12 +1493,9 @@ def convert_mount_list_to_dict( ) -> Dict[str, Dict[str, str]]: """Converts a List of (host_path, container_path) tuples to a Dict suitable as volume argument for docker sdk""" - def _map_to_dict(paths: SimpleVolumeBind | VolumeBind): - if isinstance(paths, VolumeBind): - return str(paths.host_dir), { - "bind": paths.container_dir, - "mode": "ro" if paths.read_only else "rw", - } + def _map_to_dict(paths: SimpleVolumeBind | BindMount | VolumeDirMount): + if isinstance(paths, (BindMount, VolumeDirMount)): + return paths.to_docker_sdk_parameters() else: return str(paths[0]), {"bind": paths[1], "mode": "rw"} diff --git a/localstack-core/localstack/utils/container_utils/docker_cmd_client.py b/localstack-core/localstack/utils/container_utils/docker_cmd_client.py index b65ddb2e8b018..4ca0f2d26a8c4 100644 --- a/localstack-core/localstack/utils/container_utils/docker_cmd_client.py +++ b/localstack-core/localstack/utils/container_utils/docker_cmd_client.py @@ -12,6 +12,7 @@ from localstack.utils.collections import ensure_list from localstack.utils.container_utils.container_client import ( AccessDenied, + BindMount, CancellableStream, ContainerClient, ContainerException, @@ -29,7 +30,7 @@ SimpleVolumeBind, Ulimit, Util, - VolumeBind, + VolumeDirMount, ) from localstack.utils.run import run from localstack.utils.strings import first_char_to_upper, to_str @@ -878,7 +879,7 @@ def _build_run_create_cmd( return cmd, env_file @staticmethod - def _map_to_volume_param(volume: Union[SimpleVolumeBind, VolumeBind]) -> str: + def _map_to_volume_param(volume: Union[SimpleVolumeBind, BindMount, VolumeDirMount]) -> str: """ Maps the mount volume, to a parameter for the -v docker cli argument. @@ -889,7 +890,7 @@ def _map_to_volume_param(volume: Union[SimpleVolumeBind, VolumeBind]) -> str: :param volume: Either a SimpleVolumeBind, in essence a tuple (host_dir, container_dir), or a VolumeBind object :return: String which is passable as parameter to the docker cli -v option """ - if isinstance(volume, VolumeBind): + if isinstance(volume, (BindMount, VolumeDirMount)): return volume.to_str() else: return f"{volume[0]}:{volume[1]}" diff --git a/tests/bootstrap/test_container_configurators.py b/tests/bootstrap/test_container_configurators.py index d6371d35dc6e4..6482d067facdb 100644 --- a/tests/bootstrap/test_container_configurators.py +++ b/tests/bootstrap/test_container_configurators.py @@ -9,7 +9,7 @@ get_gateway_url, ) from localstack.utils.common import external_service_ports -from localstack.utils.container_utils.container_client import VolumeBind +from localstack.utils.container_utils.container_client import BindMount def test_common_container_fixture_configurators( @@ -96,7 +96,7 @@ def test_custom_command_configurator(container_factory, tmp_path, stream_contain ContainerConfigurators.custom_command( ["/tmp/pytest-tmp-path/my-command.sh", "hello", "world"] ), - ContainerConfigurators.volume(VolumeBind(str(tmp_path), "/tmp/pytest-tmp-path")), + ContainerConfigurators.volume(BindMount(str(tmp_path), "/tmp/pytest-tmp-path")), ], remove=False, ) diff --git a/tests/bootstrap/test_init.py b/tests/bootstrap/test_init.py index 93bfad3870441..6bd4455860890 100644 --- a/tests/bootstrap/test_init.py +++ b/tests/bootstrap/test_init.py @@ -6,7 +6,7 @@ from localstack.config import in_docker from localstack.testing.pytest.container import ContainerFactory from localstack.utils.bootstrap import ContainerConfigurators -from localstack.utils.container_utils.container_client import VolumeBind +from localstack.utils.container_utils.container_client import BindMount pytestmarks = pytest.mark.skipif( condition=in_docker(), reason="cannot run bootstrap tests in docker" @@ -43,7 +43,7 @@ def test_shutdown_hooks( ContainerConfigurators.default_gateway_port, ContainerConfigurators.mount_localstack_volume(volume), ContainerConfigurators.volume( - VolumeBind(str(shutdown_hooks), "/etc/localstack/init/shutdown.d") + BindMount(str(shutdown_hooks), "/etc/localstack/init/shutdown.d") ), ] ) diff --git a/tests/unit/test_docker_utils.py b/tests/unit/test_docker_utils.py index 38a949f376dba..6f3afa121dfe9 100644 --- a/tests/unit/test_docker_utils.py +++ b/tests/unit/test_docker_utils.py @@ -1,6 +1,6 @@ from unittest import mock -from localstack.utils.container_utils.container_client import VolumeInfo +from localstack.utils.container_utils.container_client import VolumeDirMount, VolumeInfo from localstack.utils.docker_utils import get_host_path_for_path_in_docker @@ -75,3 +75,31 @@ def test_host_path_for_path_in_docker_linux_wrong_path(self): assert result == "/var/lib/localstacktest" result = get_host_path_for_path_in_docker("/etc/some/path") assert result == "/etc/some/path" + + def test_volume_dir_mount_linux(self): + with ( + mock.patch("localstack.utils.docker_utils.get_default_volume_dir_mount") as get_volume, + mock.patch("localstack.config.is_in_docker", True), + ): + get_volume.return_value = VolumeInfo( + type="bind", + source="/home/some-user/.cache/localstack/volume", + destination="/var/lib/localstack", + mode="rw", + rw=True, + propagation="rprivate", + ) + volume_dir_mount = VolumeDirMount( + "/var/lib/localstack/some/test/file", "/target/file", read_only=False + ) + result = volume_dir_mount.to_docker_sdk_parameters() + get_volume.assert_called_once() + assert result == ( + "/home/some-user/.cache/localstack/volume/some/test/file", + { + "bind": "/target/file", + "mode": "rw", + }, + ) + result = volume_dir_mount.to_str() + assert result == "/home/some-user/.cache/localstack/volume/some/test/file:/target/file" diff --git a/tests/unit/utils/test_bootstrap.py b/tests/unit/utils/test_bootstrap.py index 3da62957739d3..9ff4d2e5fc8d8 100644 --- a/tests/unit/utils/test_bootstrap.py +++ b/tests/unit/utils/test_bootstrap.py @@ -12,7 +12,7 @@ get_gateway_port, get_preloaded_services, ) -from localstack.utils.container_utils.container_client import ContainerConfiguration, VolumeBind +from localstack.utils.container_utils.container_client import BindMount, ContainerConfiguration @contextmanager @@ -246,5 +246,5 @@ def test_cli_params(self, monkeypatch): "53/udp": 53, "6000/tcp": 5000, } - assert VolumeBind(host_dir="foo", container_dir="/tmp/foo", read_only=False) in c.volumes - assert VolumeBind(host_dir="/bar", container_dir="/tmp/bar", read_only=True) in c.volumes + assert BindMount(host_dir="foo", container_dir="/tmp/foo", read_only=False) in c.volumes + assert BindMount(host_dir="/bar", container_dir="/tmp/bar", read_only=True) in c.volumes From 4fac1ca9cbb2ea285204fb30f6da4f89bdeca402 Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Tue, 8 Apr 2025 15:41:43 +0200 Subject: [PATCH 033/108] CFn: Add Lambda Function LoggingConfig (#12480) --- .../resource_providers/aws_lambda_function.py | 33 ++++++- .../aws_lambda_function.schema.json | 92 +++++++++++++++++-- .../cloudformation/resources/test_lambda.py | 32 +++++++ .../resources/test_lambda.snapshot.json | 55 +++++++++++ .../resources/test_lambda.validation.json | 3 + .../templates/cfn_lambda_logging_config.yaml | 52 +++++++++++ 6 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 tests/aws/templates/cfn_lambda_logging_config.yaml diff --git a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py index 60b9c36b4c2ac..bbcc61e335934 100644 --- a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py +++ b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.py @@ -99,6 +99,13 @@ class Code(TypedDict): ZipFile: Optional[str] +class LoggingConfig(TypedDict): + ApplicationLogLevel: Optional[str] + LogFormat: Optional[str] + LogGroup: Optional[str] + SystemLogLevel: Optional[str] + + class Environment(TypedDict): Variables: Optional[dict] @@ -333,11 +340,22 @@ def create( - ec2:DescribeSecurityGroups - ec2:DescribeSubnets - ec2:DescribeVpcs + - elasticfilesystem:DescribeMountTargets + - kms:CreateGrant - kms:Decrypt + - kms:Encrypt + - kms:GenerateDataKey - lambda:GetCodeSigningConfig - lambda:GetFunctionCodeSigningConfig + - lambda:GetLayerVersion - lambda:GetRuntimeManagementConfig - lambda:PutRuntimeManagementConfig + - lambda:TagResource + - lambda:GetPolicy + - lambda:AddPermission + - lambda:RemovePermission + - lambda:GetResourcePolicy + - lambda:PutResourcePolicy """ model = request.desired_state @@ -368,6 +386,7 @@ def create( "Timeout", "TracingConfig", "VpcConfig", + "LoggingConfig", ], ) if "Timeout" in kwargs: @@ -481,13 +500,22 @@ def update( - ec2:DescribeSecurityGroups - ec2:DescribeSubnets - ec2:DescribeVpcs + - elasticfilesystem:DescribeMountTargets + - kms:CreateGrant - kms:Decrypt + - kms:GenerateDataKey + - lambda:GetRuntimeManagementConfig + - lambda:PutRuntimeManagementConfig - lambda:PutFunctionCodeSigningConfig - lambda:DeleteFunctionCodeSigningConfig - lambda:GetCodeSigningConfig - lambda:GetFunctionCodeSigningConfig - - lambda:GetRuntimeManagementConfig - - lambda:PutRuntimeManagementConfig + - lambda:GetPolicy + - lambda:AddPermission + - lambda:RemovePermission + - lambda:GetResourcePolicy + - lambda:PutResourcePolicy + - lambda:DeleteResourcePolicy """ client = request.aws_client_factory.lambda_ @@ -512,6 +540,7 @@ def update( "Timeout", "TracingConfig", "VpcConfig", + "LoggingConfig", ] update_config_props = util.select_attributes(request.desired_state, config_keys) function_name = request.previous_state["FunctionName"] diff --git a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.schema.json b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.schema.json index a03d74999becd..b1d128047b150 100644 --- a/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.schema.json +++ b/localstack-core/localstack/services/lambda_/resource_providers/aws_lambda_function.schema.json @@ -1,4 +1,11 @@ { + "tagging": { + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "tagProperty": "/properties/Tags", + "cloudFormationSystemTags": true + }, "handlers": { "read": { "permissions": [ @@ -17,11 +24,22 @@ "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs", + "elasticfilesystem:DescribeMountTargets", + "kms:CreateGrant", "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey", "lambda:GetCodeSigningConfig", "lambda:GetFunctionCodeSigningConfig", + "lambda:GetLayerVersion", "lambda:GetRuntimeManagementConfig", - "lambda:PutRuntimeManagementConfig" + "lambda:PutRuntimeManagementConfig", + "lambda:TagResource", + "lambda:GetPolicy", + "lambda:AddPermission", + "lambda:RemovePermission", + "lambda:GetResourcePolicy", + "lambda:PutResourcePolicy" ] }, "update": { @@ -40,13 +58,22 @@ "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs", + "elasticfilesystem:DescribeMountTargets", + "kms:CreateGrant", "kms:Decrypt", + "kms:GenerateDataKey", + "lambda:GetRuntimeManagementConfig", + "lambda:PutRuntimeManagementConfig", "lambda:PutFunctionCodeSigningConfig", "lambda:DeleteFunctionCodeSigningConfig", "lambda:GetCodeSigningConfig", "lambda:GetFunctionCodeSigningConfig", - "lambda:GetRuntimeManagementConfig", - "lambda:PutRuntimeManagementConfig" + "lambda:GetPolicy", + "lambda:AddPermission", + "lambda:RemovePermission", + "lambda:GetResourcePolicy", + "lambda:PutResourcePolicy", + "lambda:DeleteResourcePolicy" ] }, "list": { @@ -63,13 +90,15 @@ }, "typeName": "AWS::Lambda::Function", "readOnlyProperties": [ - "/properties/Arn", "/properties/SnapStartResponse", "/properties/SnapStartResponse/ApplyOn", - "/properties/SnapStartResponse/OptimizationStatus" + "/properties/SnapStartResponse/OptimizationStatus", + "/properties/Arn" ], - "description": "Resource Type definition for AWS::Lambda::Function", + "description": "Resource Type definition for AWS::Lambda::Function in region", "writeOnlyProperties": [ + "/properties/SnapStart", + "/properties/SnapStart/ApplyOn", "/properties/Code", "/properties/Code/ImageUri", "/properties/Code/S3Bucket", @@ -133,6 +162,10 @@ "additionalProperties": false, "type": "object", "properties": { + "Ipv6AllowedForDualStack": { + "description": "A boolean indicating whether IPv6 protocols will be allowed for dual stack subnets", + "type": "boolean" + }, "SecurityGroupIds": { "maxItems": 5, "uniqueItems": false, @@ -261,6 +294,49 @@ } } }, + "LoggingConfig": { + "description": "The function's logging configuration.", + "additionalProperties": false, + "type": "object", + "properties": { + "LogFormat": { + "description": "Log delivery format for the lambda function", + "type": "string", + "enum": [ + "Text", + "JSON" + ] + }, + "ApplicationLogLevel": { + "description": "Application log granularity level, can only be used when LogFormat is set to JSON", + "type": "string", + "enum": [ + "TRACE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "FATAL" + ] + }, + "LogGroup": { + "minLength": 1, + "pattern": "[\\.\\-_/#A-Za-z0-9]+", + "description": "The log group name.", + "type": "string", + "maxLength": 512 + }, + "SystemLogLevel": { + "description": "System log granularity level, can only be used when LogFormat is set to JSON", + "type": "string", + "enum": [ + "DEBUG", + "INFO", + "WARN" + ] + } + } + }, "Environment": { "description": "A function's environment variable settings.", "additionalProperties": false, @@ -457,6 +533,10 @@ "description": "The Amazon Resource Name (ARN) of the function's execution role.", "type": "string" }, + "LoggingConfig": { + "description": "The logging configuration of your function", + "$ref": "#/definitions/LoggingConfig" + }, "Environment": { "description": "Environment variables that are accessible from function code during execution.", "$ref": "#/definitions/Environment" diff --git a/tests/aws/services/cloudformation/resources/test_lambda.py b/tests/aws/services/cloudformation/resources/test_lambda.py index 527a3321540ba..532ea5a11436d 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.py +++ b/tests/aws/services/cloudformation/resources/test_lambda.py @@ -253,6 +253,38 @@ def test_lambda_alias(deploy_cfn_template, snapshot, aws_client): snapshot.match("Alias", alias) +@markers.aws.validated +def test_lambda_logging_config(deploy_cfn_template, snapshot, aws_client): + function_name = f"function{short_uid()}" + + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + snapshot.add_transformer(SortingTransformer("StackResources", lambda x: x["LogicalResourceId"])) + snapshot.add_transformer( + snapshot.transform.key_value("LogicalResourceId", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("PhysicalResourceId", reference_replacement=False) + ) + snapshot.add_transformer(snapshot.transform.regex(function_name, "")) + + deployment = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/cfn_lambda_logging_config.yaml" + ), + parameters={"FunctionName": function_name}, + ) + + description = aws_client.cloudformation.describe_stack_resources( + StackName=deployment.stack_name + ) + snapshot.match("stack_resource_descriptions", description) + + logging_config = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ + "LoggingConfig" + ] + snapshot.match("logging_config", logging_config) + + @pytest.mark.skipif( not in_default_partition(), reason="Test not applicable in non-default partitions" ) diff --git a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json index c61888dca606a..d3e39608a2b41 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json @@ -1606,5 +1606,60 @@ "LayerVersionRef": "arn::lambda::111111111111:layer::1" } } + }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_logging_config": { + "recorded-date": "08-04-2025, 12:10:56", + "recorded-content": { + "stack_resource_descriptions": { + "StackResources": [ + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "logical-resource-id", + "PhysicalResourceId": "physical-resource-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::Lambda::Function", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "logical-resource-id", + "PhysicalResourceId": "physical-resource-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::IAM::Role", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "logical-resource-id", + "PhysicalResourceId": "physical-resource-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::Lambda::Version", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "logging_config": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + } + } } } diff --git a/tests/aws/services/cloudformation/resources/test_lambda.validation.json b/tests/aws/services/cloudformation/resources/test_lambda.validation.json index 74611cffac904..910fd07381eec 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.validation.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.validation.json @@ -41,6 +41,9 @@ "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_layer_crud": { "last_validated_date": "2024-12-20T18:23:31+00:00" }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_logging_config": { + "last_validated_date": "2025-04-08T12:12:01+00:00" + }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": { "last_validated_date": "2024-04-09T07:21:37+00:00" }, diff --git a/tests/aws/templates/cfn_lambda_logging_config.yaml b/tests/aws/templates/cfn_lambda_logging_config.yaml new file mode 100644 index 0000000000000..547b60f9466c9 --- /dev/null +++ b/tests/aws/templates/cfn_lambda_logging_config.yaml @@ -0,0 +1,52 @@ +AWSTemplateFormatVersion: 2010-09-09 + +Parameters: + FunctionName: + Type: String + +Resources: + MyFnServiceRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + + LambdaFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Ref FunctionName + Code: + ZipFile: | + def handler(event, context): + return { + statusCode: 200, + body: "Hello, World!" + } + Role: + Fn::GetAtt: + - MyFnServiceRole + - Arn + Handler: index.handler + Runtime: python3.12 + LoggingConfig: + LogFormat: JSON + DependsOn: + - MyFnServiceRole + + Version: + Type: AWS::Lambda::Version + Properties: + FunctionName: !Ref LambdaFunction + Description: v1 + From 6dc9c5439e052f5cfdfc750c50e13c4a8c084784 Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:09:54 +0200 Subject: [PATCH 034/108] Added github action to validate feature catalog files (#12475) --- .github/workflows/pr-validate-features-files.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/pr-validate-features-files.yml diff --git a/.github/workflows/pr-validate-features-files.yml b/.github/workflows/pr-validate-features-files.yml new file mode 100644 index 0000000000000..d62d2b5ffaa77 --- /dev/null +++ b/.github/workflows/pr-validate-features-files.yml @@ -0,0 +1,14 @@ +name: Validate AWS features files + +on: + pull_request: + paths: + - localstack-core/localstack/services/** + branches: + - master + +jobs: + validate-features-files: + uses: localstack/meta/.github/workflows/pr-validate-features-files.yml@main + with: + aws_services_path: 'localstack-core/localstack/services' From 807b69d421f5ff68597ffb519a7a4bd7c374d118 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:13:07 +0530 Subject: [PATCH 035/108] KMS: add ability to decrypt data with all rotated keys (#12482) --- .../localstack/services/kms/models.py | 25 ++++++-- .../localstack/services/kms/provider.py | 2 +- tests/aws/services/kms/test_kms.py | 64 ++++++++++++++++++- tests/aws/services/kms/test_kms.snapshot.json | 22 +++++++ .../aws/services/kms/test_kms.validation.json | 6 ++ 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/services/kms/models.py b/localstack-core/localstack/services/kms/models.py index f923d7433e14b..91f62a542ec07 100644 --- a/localstack-core/localstack/services/kms/models.py +++ b/localstack-core/localstack/services/kms/models.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from typing import Dict, Optional, Tuple -from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.exceptions import InvalidSignature, InvalidTag, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac from cryptography.hazmat.primitives import serialization as crypto_serialization @@ -29,6 +29,7 @@ CreateGrantRequest, CreateKeyRequest, EncryptionContextType, + InvalidCiphertextException, InvalidKeyUsageException, KeyMetadata, KeySpec, @@ -36,6 +37,7 @@ KeyUsageType, KMSInvalidMacException, KMSInvalidSignatureException, + LimitExceededException, MacAlgorithmSpec, MessageType, MultiRegionConfiguration, @@ -84,6 +86,7 @@ "HMAC_512": (64, 128), } +ON_DEMAND_ROTATION_LIMIT = 10 KEY_ID_LEN = 36 # Moto uses IV_LEN of 12, as it is fine for GCM encryption mode, but we use CBC, so have to set it to 16. IV_LEN = 16 @@ -249,6 +252,7 @@ class KmsKey: is_key_rotation_enabled: bool rotation_period_in_days: int next_rotation_date: datetime.datetime + previous_keys = [str] def __init__( self, @@ -257,6 +261,7 @@ def __init__( region: str = None, ): create_key_request = create_key_request or CreateKeyRequest() + self.previous_keys = [] # Please keep in mind that tags of a key could be present in the request, they are not a part of metadata. At # least in the sense of DescribeKey not returning them with the rest of the metadata. Instead, tags are more @@ -319,9 +324,15 @@ def decrypt( self, ciphertext: Ciphertext, encryption_context: EncryptionContextType = None ) -> bytes: aad = _serialize_encryption_context(encryption_context=encryption_context) - return decrypt( - self.crypto_key.key_material, ciphertext.ciphertext, ciphertext.iv, ciphertext.tag, aad - ) + keys_to_try = [self.crypto_key.key_material] + self.previous_keys + + for key in keys_to_try: + try: + return decrypt(key, ciphertext.ciphertext, ciphertext.iv, ciphertext.tag, aad) + except (InvalidTag, InvalidSignature): + continue + + raise InvalidCiphertextException() def decrypt_rsa(self, encrypted: bytes) -> bytes: private_key = crypto_serialization.load_der_private_key( @@ -694,6 +705,12 @@ def _get_key_usage(self, request_key_usage: str, key_spec: str) -> str: return request_key_usage or "ENCRYPT_DECRYPT" def rotate_key_on_demand(self): + if len(self.previous_keys) >= ON_DEMAND_ROTATION_LIMIT: + raise LimitExceededException( + f"The on-demand rotations limit has been reached for the given keyId. " + f"No more on-demand rotations can be performed for this key: {self.metadata['Arn']}" + ) + self.previous_keys.append(self.crypto_key.key_material) self.crypto_key = KmsCryptoKey(KeySpec.SYMMETRIC_DEFAULT) diff --git a/localstack-core/localstack/services/kms/provider.py b/localstack-core/localstack/services/kms/provider.py index 342715dc0710f..cb285626a092c 100644 --- a/localstack-core/localstack/services/kms/provider.py +++ b/localstack-core/localstack/services/kms/provider.py @@ -1341,7 +1341,7 @@ def list_resource_tags( return ListResourceTagsResponse(Tags=page, **kwargs) @handler("RotateKeyOnDemand", expand=False) - # TODO: keep trak of key rotations as AWS does and return them in the ListKeyRotations operation + # TODO: return the key rotations in the ListKeyRotations operation def rotate_key_on_demand( self, context: RequestContext, request: RotateKeyOnDemandRequest ) -> RotateKeyOnDemandResponse: diff --git a/tests/aws/services/kms/test_kms.py b/tests/aws/services/kms/test_kms.py index 2304b20cc0305..bd9b5eab08cc3 100644 --- a/tests/aws/services/kms/test_kms.py +++ b/tests/aws/services/kms/test_kms.py @@ -13,7 +13,12 @@ from cryptography.hazmat.primitives.asymmetric import ec, padding, utils from cryptography.hazmat.primitives.serialization import load_der_public_key -from localstack.services.kms.models import IV_LEN, Ciphertext, _serialize_ciphertext_blob +from localstack.services.kms.models import ( + IV_LEN, + ON_DEMAND_ROTATION_LIMIT, + Ciphertext, + _serialize_ciphertext_blob, +) from localstack.services.kms.utils import get_hash_algorithm from localstack.testing.aws.util import in_default_partition from localstack.testing.pytest import markers @@ -1154,6 +1159,63 @@ def test_key_rotation_status(self, kms_key, aws_client): aws_client.kms.disable_key_rotation(KeyId=key_id) assert aws_client.kms.get_key_rotation_status(KeyId=key_id)["KeyRotationEnabled"] is False + @markers.aws.validated + def test_key_rotations_encryption_decryption(self, kms_create_key, aws_client, snapshot): + key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec="SYMMETRIC_DEFAULT")["KeyId"] + message = b"test message 123 !%$@ 1234567890" + + ciphertext = aws_client.kms.encrypt( + KeyId=key_id, + Plaintext=base64.b64encode(message), + EncryptionAlgorithm="SYMMETRIC_DEFAULT", + )["CiphertextBlob"] + + deciphered_text_before = aws_client.kms.decrypt( + KeyId=key_id, + CiphertextBlob=ciphertext, + EncryptionAlgorithm="SYMMETRIC_DEFAULT", + )["Plaintext"] + + aws_client.kms.rotate_key_on_demand(KeyId=key_id) + + deciphered_text_after = aws_client.kms.decrypt( + KeyId=key_id, + CiphertextBlob=ciphertext, + EncryptionAlgorithm="SYMMETRIC_DEFAULT", + )["Plaintext"] + + assert deciphered_text_after == deciphered_text_before + + # checking for the exception + bad_ciphertext = ciphertext + b"bad_data" + + with pytest.raises(ClientError) as e: + aws_client.kms.decrypt( + KeyId=key_id, + CiphertextBlob=bad_ciphertext, + EncryptionAlgorithm="SYMMETRIC_DEFAULT", + ) + + snapshot.match("bad-ciphertext", e.value) + + @markers.aws.validated + def test_key_rotations_limits(self, kms_create_key, aws_client, snapshot): + key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec="SYMMETRIC_DEFAULT")["KeyId"] + + def _assert_on_demand_rotation_completed(): + response = aws_client.kms.get_key_rotation_status(KeyId=key_id) + return "OnDemandRotationStartDate" not in response + + for _ in range(ON_DEMAND_ROTATION_LIMIT): + aws_client.kms.rotate_key_on_demand(KeyId=key_id) + assert poll_condition( + condition=_assert_on_demand_rotation_completed, timeout=10, interval=1 + ) + + with pytest.raises(ClientError) as e: + aws_client.kms.rotate_key_on_demand(KeyId=key_id) + snapshot.match("error-response", e.value.response) + @markers.aws.validated def test_rotate_key_on_demand_modifies_key_material(self, kms_create_key, aws_client, snapshot): key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec="SYMMETRIC_DEFAULT")["KeyId"] diff --git a/tests/aws/services/kms/test_kms.snapshot.json b/tests/aws/services/kms/test_kms.snapshot.json index e10807820c82a..fea96abd3ab31 100644 --- a/tests/aws/services/kms/test_kms.snapshot.json +++ b/tests/aws/services/kms/test_kms.snapshot.json @@ -2184,5 +2184,27 @@ "tests/aws/services/kms/test_kms.py::TestKMS::test_verify_salt_length[ECC_SECG_P256K1-ECDSA_SHA_256]": { "recorded-date": "02-04-2025, 06:07:08", "recorded-content": {} + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotations_encryption_decryption": { + "recorded-date": "03-04-2025, 09:34:48", + "recorded-content": { + "bad-ciphertext": "An error occurred (InvalidCiphertextException) when calling the Decrypt operation: " + } + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotations_limits": { + "recorded-date": "03-04-2025, 11:10:33", + "recorded-content": { + "error-response": { + "Error": { + "Code": "LimitExceededException", + "Message": "The on-demand rotations limit has been reached for the given keyId. No more on-demand rotations can be performed for this key: " + }, + "message": "The on-demand rotations limit has been reached for the given keyId. No more on-demand rotations can be performed for this key: ", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/kms/test_kms.validation.json b/tests/aws/services/kms/test_kms.validation.json index 3dfb4c2682b53..419b36f95854f 100644 --- a/tests/aws/services/kms/test_kms.validation.json +++ b/tests/aws/services/kms/test_kms.validation.json @@ -176,6 +176,12 @@ "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotation_status": { "last_validated_date": "2024-04-11T15:53:48+00:00" }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotations_encryption_decryption": { + "last_validated_date": "2025-04-03T09:34:47+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotations_limits": { + "last_validated_date": "2025-04-03T11:10:33+00:00" + }, "tests/aws/services/kms/test_kms.py::TestKMS::test_key_with_long_tag_value_raises_error": { "last_validated_date": "2025-01-21T17:18:18+00:00" }, From 377b1a79f7a1780baab42a5c4c2779971128410e Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:06:58 +0200 Subject: [PATCH 036/108] CloudFormation: Update Graph Preprocessor (#12447) Co-authored-by: Simon Walker --- .github/workflows/marker-report.yml | 4 +- .github/workflows/tests-cli.yml | 2 +- .../cloudformation/engine/entities.py | 13 +- .../engine/v2/change_set_model.py | 99 +- .../engine/v2/change_set_model_describer.py | 606 +-- .../engine/v2/change_set_model_executor.py | 177 +- .../engine/v2/change_set_model_preproc.py | 574 +++ .../engine/v2/change_set_model_visitor.py | 12 +- .../services/cloudformation/v2/provider.py | 44 +- .../services/cloudformation/v2/utils.py | 5 + .../sns/resource_providers/aws_sns_topic.py | 1 + .../testing/pytest/cloudformation/__init__.py | 0 .../testing/pytest/cloudformation/fixtures.py | 169 + .../cloudformation/api/test_changesets.py | 439 +- .../api/test_changesets.snapshot.json | 4059 +++++++++++++---- .../api/test_changesets.validation.json | 31 +- .../v2/test_change_set_fn_get_attr.py | 313 ++ .../test_change_set_fn_get_attr.snapshot.json | 3020 ++++++++++++ ...est_change_set_fn_get_attr.validation.json | 20 + .../cloudformation/v2/test_change_set_ref.py | 309 ++ .../v2/test_change_set_ref.snapshot.json | 2444 ++++++++++ .../v2/test_change_set_ref.validation.json | 17 + tests/conftest.py | 1 + .../test_change_set_describe_details.py | 144 +- 24 files changed, 10783 insertions(+), 1720 deletions(-) create mode 100644 localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py create mode 100644 localstack-core/localstack/services/cloudformation/v2/utils.py create mode 100644 localstack-core/localstack/testing/pytest/cloudformation/__init__.py create mode 100644 localstack-core/localstack/testing/pytest/cloudformation/fixtures.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.validation.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_ref.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json diff --git a/.github/workflows/marker-report.yml b/.github/workflows/marker-report.yml index 75b5352891324..6992be9827954 100644 --- a/.github/workflows/marker-report.yml +++ b/.github/workflows/marker-report.yml @@ -60,7 +60,7 @@ jobs: - name: Collect marker report if: ${{ !inputs.createIssue }} env: - PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s --co --disable-warnings --marker-report --marker-report-tinybird-upload" + PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -p no:localstack.testing.pytest.cloudformation.fixtures -s --co --disable-warnings --marker-report --marker-report-tinybird-upload" MARKER_REPORT_PROJECT_NAME: localstack MARKER_REPORT_TINYBIRD_TOKEN: ${{ secrets.MARKER_REPORT_TINYBIRD_TOKEN }} MARKER_REPORT_COMMIT_SHA: ${{ github.sha }} @@ -71,7 +71,7 @@ jobs: # makes use of the marker report plugin localstack.testing.pytest.marker_report - name: Generate marker report env: - PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s --co --disable-warnings --marker-report --marker-report-path './target'" + PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -p no:localstack.testing.pytest.cloudformation.fixtures -p no: -s --co --disable-warnings --marker-report --marker-report-path './target'" MARKER_REPORT_PROJECT_NAME: localstack MARKER_REPORT_COMMIT_SHA: ${{ github.sha }} run: | diff --git a/.github/workflows/tests-cli.yml b/.github/workflows/tests-cli.yml index a1a3051fc7893..9dda7f376e9d1 100644 --- a/.github/workflows/tests-cli.yml +++ b/.github/workflows/tests-cli.yml @@ -98,7 +98,7 @@ jobs: pip install pytest pytest-tinybird - name: Run CLI tests env: - PYTEST_ADDOPTS: "${{ env.TINYBIRD_PYTEST_ARGS }}-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:localstack.testing.pytest.validation_tracking -p no:localstack.testing.pytest.path_filter -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s" + PYTEST_ADDOPTS: "${{ env.TINYBIRD_PYTEST_ARGS }}-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:localstack.testing.pytest.validation_tracking -p no:localstack.testing.pytest.path_filter -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -p no:localstack.testing.pytest.cloudformation.fixtures -s" TEST_PATH: "tests/cli/" run: make test diff --git a/localstack-core/localstack/services/cloudformation/engine/entities.py b/localstack-core/localstack/services/cloudformation/engine/entities.py index 3df7f8ea19195..3083d5a2c0363 100644 --- a/localstack-core/localstack/services/cloudformation/engine/entities.py +++ b/localstack-core/localstack/services/cloudformation/engine/entities.py @@ -422,12 +422,17 @@ def changes(self): return result # V2 only - def populate_update_graph(self, before_template: dict | None, after_template: dict | None): + def populate_update_graph( + self, + before_template: Optional[dict], + after_template: Optional[dict], + before_parameters: Optional[dict], + after_parameters: Optional[dict], + ) -> None: change_set_model = ChangeSetModel( before_template=before_template, after_template=after_template, - # TODO - before_parameters=None, - after_parameters=None, + before_parameters=before_parameters, + after_parameters=after_parameters, ) self.update_graph = change_set_model.get_update_model() diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index a9a3fdc4fe15d..3d65187172611 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -7,7 +7,6 @@ from typing_extensions import TypeVar -from localstack.aws.api.cloudformation import ChangeAction from localstack.utils.strings import camel_to_snake_case T = TypeVar("T") @@ -67,15 +66,6 @@ class ChangeType(enum.Enum): def __str__(self): return self.value - def to_action(self) -> ChangeAction | None: - match self: - case self.CREATED: - return ChangeAction.Add - case self.MODIFIED: - return ChangeAction.Modify - case self.REMOVED: - return ChangeAction.Remove - def for_child(self, child_change_type: ChangeType) -> ChangeType: if child_change_type == self: return self @@ -525,7 +515,7 @@ def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeT node_parameter = self._retrieve_parameter_if_exists(parameter_name=logical_id) if isinstance(node_parameter, NodeParameter): - return node_parameter.dynamic_value.change_type + return node_parameter.change_type # TODO: this should check the replacement flag for a resource update. node_resource = self._retrieve_or_visit_resource(resource_name=logical_id) @@ -584,19 +574,16 @@ def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> Chang def _visit_array( self, scope: Scope, before_array: Maybe[list], after_array: Maybe[list] ) -> NodeArray: - change_type = ChangeType.UNCHANGED array: list[ChangeSetEntity] = list() for index, (before_value, after_value) in enumerate( zip_longest(before_array, after_array, fillvalue=Nothing) ): - # TODO: should extract this scoping logic. value_scope = scope.open_index(index=index) value = self._visit_value( scope=value_scope, before_value=before_value, after_value=after_value ) array.append(value) - if value.change_type != ChangeType.UNCHANGED: - change_type = ChangeType.MODIFIED + change_type = self._change_type_for_parent_of([value.change_type for value in array]) return NodeArray(scope=scope, change_type=change_type, array=array) def _visit_object( @@ -695,28 +682,12 @@ def _visit_property( node_property = self._visited_scopes.get(scope) if isinstance(node_property, NodeProperty): return node_property - value = self._visit_value( scope=scope, before_value=before_property, after_value=after_property ) - if self._is_created(before=before_property, after=after_property): - node_property = NodeProperty( - scope=scope, - change_type=ChangeType.CREATED, - name=property_name, - value=value, - ) - elif self._is_removed(before=before_property, after=after_property): - node_property = NodeProperty( - scope=scope, - change_type=ChangeType.REMOVED, - name=property_name, - value=value, - ) - else: - node_property = NodeProperty( - scope=scope, change_type=value.change_type, name=property_name, value=value - ) + node_property = NodeProperty( + scope=scope, change_type=value.change_type, name=property_name, value=value + ) self._visited_scopes[scope] = node_property return node_property @@ -748,6 +719,13 @@ def _visit_properties( self._visited_scopes[scope] = node_properties return node_properties + def _visit_type(self, scope: Scope, before_type: Any, after_type: Any) -> TerminalValue: + value = self._visit_value(scope=scope, before_value=before_type, after_value=after_type) + if not isinstance(value, TerminalValue): + # TODO: decide where template schema validation should occur. + raise RuntimeError() + return value + def _visit_resource( self, scope: Scope, @@ -766,8 +744,12 @@ def _visit_resource( else: change_type = ChangeType.UNCHANGED - # TODO: investigate behaviour with type changes, for now this is filler code. - _, type_str = self._safe_access_in(scope, TypeKey, after_resource) + scope_type, (before_type, after_type) = self._safe_access_in( + scope, TypeKey, before_resource, after_resource + ) + terminal_value_type = self._visit_type( + scope=scope_type, before_type=before_type, after_type=after_type + ) condition_reference = None scope_condition, (before_condition, after_condition) = self._safe_access_in( @@ -792,7 +774,7 @@ def _visit_resource( scope=scope, change_type=change_type, name=resource_name, - type_=TerminalValueUnchanged(scope=scope, value=type_str), + type_=terminal_value_type, condition_reference=condition_reference, properties=properties, ) @@ -872,36 +854,19 @@ def _visit_parameter( return node_parameter # TODO: add logic to compute defaults already in the graph building process? dynamic_value = self._visit_dynamic_parameter(parameter_name=parameter_name) - if self._is_created(before=before_parameter, after=after_parameter): - node_parameter = NodeParameter( - scope=scope, - change_type=ChangeType.CREATED, - name=parameter_name, - value=TerminalValueCreated(scope=scope, value=after_parameter), - dynamic_value=dynamic_value, - ) - elif self._is_removed(before=before_parameter, after=after_parameter): - node_parameter = NodeParameter( - scope=scope, - change_type=ChangeType.REMOVED, - name=parameter_name, - value=TerminalValueRemoved(scope=scope, value=before_parameter), - dynamic_value=dynamic_value, - ) - else: - value = self._visit_value( - scope=scope, before_value=before_parameter, after_value=after_parameter - ) - change_type = self._change_type_for_parent_of( - change_types=[dynamic_value.change_type, value.change_type] - ) - node_parameter = NodeParameter( - scope=scope, - change_type=change_type, - name=parameter_name, - value=value, - dynamic_value=dynamic_value, - ) + value = self._visit_value( + scope=scope, before_value=before_parameter, after_value=after_parameter + ) + change_type = self._change_type_for_parent_of( + change_types=[dynamic_value.change_type, value.change_type] + ) + node_parameter = NodeParameter( + scope=scope, + change_type=change_type, + name=parameter_name, + value=value, + dynamic_value=dynamic_value, + ) self._visited_scopes[scope] = node_parameter return node_parameter diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index 7b114fed93a66..35115f4ceb05c 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -1,230 +1,50 @@ from __future__ import annotations -import abc -from typing import Any, Final, Optional +import json +from typing import Final, Optional import localstack.aws.api.cloudformation as cfn_api from localstack.services.cloudformation.engine.v2.change_set_model import ( - ChangeSetEntity, - ChangeType, - ConditionKey, - ExportKey, - NodeArray, - NodeCondition, - NodeDivergence, NodeIntrinsicFunction, - NodeMapping, - NodeObject, - NodeOutput, - NodeOutputs, - NodeParameter, - NodeProperties, - NodeProperty, NodeResource, NodeTemplate, - NothingType, PropertiesKey, - Scope, - TerminalValue, - TerminalValueCreated, - TerminalValueModified, - TerminalValueRemoved, - TerminalValueUnchanged, - ValueKey, ) -from localstack.services.cloudformation.engine.v2.change_set_model_visitor import ( - ChangeSetModelVisitor, +from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( + ChangeSetModelPreproc, + PreprocEntityDelta, + PreprocProperties, + PreprocResource, ) CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}" -class DescribeUnit(abc.ABC): - before_context: Optional[Any] = None - after_context: Optional[Any] = None - - def __init__(self, before_context: Optional[Any] = None, after_context: Optional[Any] = None): - self.before_context = before_context - self.after_context = after_context - - -class ChangeSetModelDescriber(ChangeSetModelVisitor): - _node_template: Final[NodeTemplate] +class ChangeSetModelDescriber(ChangeSetModelPreproc): + _include_property_values: Final[bool] _changes: Final[cfn_api.Changes] - _describe_unit_cache: dict[Scope, DescribeUnit] - _include_property_values: Final[cfn_api.IncludePropertyValues | None] - def __init__( - self, - node_template: NodeTemplate, - include_property_values: cfn_api.IncludePropertyValues | None = None, - ): - self._node_template = node_template - self._changes = list() - self._describe_unit_cache = dict() + def __init__(self, node_template: NodeTemplate, include_property_values: bool): + super().__init__(node_template=node_template) self._include_property_values = include_property_values + self._changes = list() def get_changes(self) -> cfn_api.Changes: - self.visit(self._node_template) + self._changes.clear() + self.process() return self._changes - @staticmethod - def _get_node_resource_for(resource_name: str, node_template: NodeTemplate) -> NodeResource: - # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists. - for node_resource in node_template.resources.resources: - if node_resource.name == resource_name: - return node_resource - # TODO - raise RuntimeError() - - @staticmethod - def _get_node_property_for(property_name: str, node_resource: NodeResource) -> NodeProperty: - # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists. - for node_property in node_resource.properties.properties: - if node_property.name == property_name: - return node_property - # TODO - raise RuntimeError() - - def _get_node_mapping(self, map_name: str) -> NodeMapping: - mappings: list[NodeMapping] = self._node_template.mappings.mappings - # TODO: another scenarios suggesting property lookups might be preferable. - for mapping in mappings: - if mapping.name == map_name: - return mapping - # TODO - raise RuntimeError() - - def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]: - parameters: list[NodeParameter] = self._node_template.parameters.parameters - # TODO: another scenarios suggesting property lookups might be preferable. - for parameter in parameters: - if parameter.name == parameter_name: - return parameter - return None - - def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]: - conditions: list[NodeCondition] = self._node_template.conditions.conditions - # TODO: another scenarios suggesting property lookups might be preferable. - for condition in conditions: - if condition.name == condition_name: - return condition - return None - - def _resolve_reference(self, logica_id: str) -> DescribeUnit: - node_condition = self._get_node_condition_if_exists(condition_name=logica_id) - if isinstance(node_condition, NodeCondition): - condition_unit = self.visit(node_condition) - return condition_unit - - node_parameter = self._get_node_parameter_if_exists(parameter_name=logica_id) - if isinstance(node_parameter, NodeParameter): - parameter_unit = self.visit(node_parameter) - return parameter_unit - - # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - node_resource = self._get_node_resource_for( - resource_name=logica_id, node_template=self._node_template - ) - resource_unit = self.visit(node_resource) - before_context = resource_unit.before_context - after_context = resource_unit.after_context - return DescribeUnit(before_context=before_context, after_context=after_context) - - def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit: - # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids. - node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name) - top_level_value = node_mapping.bindings.bindings.get(top_level_key) - if not isinstance(top_level_value, NodeObject): - raise RuntimeError() - second_level_value = top_level_value.bindings.get(second_level_key) - mapping_value_unit = self.visit(second_level_value) - return mapping_value_unit - - def _resolve_reference_binding( - self, before_logical_id: str, after_logical_id: str - ) -> DescribeUnit: - before_unit = self._resolve_reference(logica_id=before_logical_id) - after_unit = self._resolve_reference(logica_id=after_logical_id) - return DescribeUnit( - before_context=before_unit.before_context, after_context=after_unit.after_context - ) - - def visit(self, change_set_entity: ChangeSetEntity) -> DescribeUnit: - describe_unit = self._describe_unit_cache.get(change_set_entity.scope) - if describe_unit is not None: - return describe_unit - describe_unit = super().visit(change_set_entity=change_set_entity) - self._describe_unit_cache[change_set_entity.scope] = describe_unit - return describe_unit - - def visit_terminal_value_modified( - self, terminal_value_modified: TerminalValueModified - ) -> DescribeUnit: - return DescribeUnit( - before_context=terminal_value_modified.value, - after_context=terminal_value_modified.modified_value, - ) - - def visit_terminal_value_created( - self, terminal_value_created: TerminalValueCreated - ) -> DescribeUnit: - return DescribeUnit(after_context=terminal_value_created.value) - - def visit_terminal_value_removed( - self, terminal_value_removed: TerminalValueRemoved - ) -> DescribeUnit: - return DescribeUnit(before_context=terminal_value_removed.value) - - def visit_terminal_value_unchanged( - self, terminal_value_unchanged: TerminalValueUnchanged - ) -> DescribeUnit: - return DescribeUnit( - before_context=terminal_value_unchanged.value, - after_context=terminal_value_unchanged.value, - ) - - def visit_node_divergence(self, node_divergence: NodeDivergence) -> DescribeUnit: - before_unit = self.visit(node_divergence.value) - after_unit = self.visit(node_divergence.divergence) - return DescribeUnit( - before_context=before_unit.before_context, after_context=after_unit.after_context - ) - - def visit_node_object(self, node_object: NodeObject) -> DescribeUnit: - # TODO: improve check syntax - if len(node_object.bindings) == 1: - binding_values = list(node_object.bindings.values()) - unique_value = binding_values[0] - if isinstance(unique_value, NodeIntrinsicFunction): - return self.visit(unique_value) - - before_context = dict() - after_context = dict() - for name, change_set_entity in node_object.bindings.items(): - describe_unit: DescribeUnit = self.visit(change_set_entity=change_set_entity) - match change_set_entity.change_type: - case ChangeType.MODIFIED: - before_context[name] = describe_unit.before_context - after_context[name] = describe_unit.after_context - case ChangeType.CREATED: - after_context[name] = describe_unit.after_context - case ChangeType.REMOVED: - before_context[name] = describe_unit.before_context - case ChangeType.UNCHANGED: - before_context[name] = describe_unit.before_context - after_context[name] = describe_unit.before_context - return DescribeUnit(before_context=before_context, after_context=after_context) - def visit_node_intrinsic_function_fn_get_att( self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - arguments_unit = self.visit(node_intrinsic_function.arguments) - # TODO: validate the return value according to the spec. - before_argument_list = arguments_unit.before_context - after_argument_list = arguments_unit.after_context + ) -> PreprocEntityDelta: + # TODO: If we can properly compute the before and after value, why should we + # artificially limit the precision of our output to match AWS's? - before_context = None + arguments_delta = self.visit(node_intrinsic_function.arguments) + before_argument_list = arguments_delta.before + after_argument_list = arguments_delta.after + + before = None if before_argument_list: before_logical_name_of_resource = before_argument_list[0] before_attribute_name = before_argument_list[1] @@ -234,300 +54,112 @@ def visit_node_intrinsic_function_fn_get_att( before_node_property = self._get_node_property_for( property_name=before_attribute_name, node_resource=before_node_resource ) - before_property_unit = self.visit(before_node_property) - before_context = before_property_unit.before_context + before_property_delta = self.visit(before_node_property) + before = before_property_delta.before - after_context = None + after = None if after_argument_list: - after_context = CHANGESET_KNOWN_AFTER_APPLY - # TODO: the following is the logic to resolve the attribute in the `after` template - # this should be moved to the new base class and then be masked in this describer. - # after_logical_name_of_resource = after_argument_list[0] - # after_attribute_name = after_argument_list[1] - # after_node_resource = self._get_node_resource_for( - # resource_name=after_logical_name_of_resource, node_template=self._node_template - # ) - # after_node_property = self._get_node_property_for( - # property_name=after_attribute_name, node_resource=after_node_resource - # ) - # after_property_unit = self.visit(after_node_property) - # after_context = after_property_unit.after_context - - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_intrinsic_function_fn_equals( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - arguments_unit = self.visit(node_intrinsic_function.arguments) - before_values = arguments_unit.before_context - after_values = arguments_unit.after_context - before_context = None - if before_values: - before_context = before_values[0] == before_values[1] - after_context = None - if after_values: - after_context = after_values[0] == after_values[1] - match node_intrinsic_function.change_type: - case ChangeType.MODIFIED: - return DescribeUnit(before_context=before_context, after_context=after_context) - case ChangeType.CREATED: - return DescribeUnit(after_context=after_context) - case ChangeType.REMOVED: - return DescribeUnit(before_context=before_context) - # Unchanged - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_intrinsic_function_fn_if( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - arguments_unit = self.visit(node_intrinsic_function.arguments) - - def _compute_unit_for_if_statement(args: list[Any]) -> DescribeUnit: - condition_name = args[0] - boolean_expression_unit = self._resolve_reference(logica_id=condition_name) - return DescribeUnit( - before_context=args[1] if boolean_expression_unit.before_context else args[2], - after_context=args[1] if boolean_expression_unit.after_context else args[2], + after_logical_name_of_resource = after_argument_list[0] + after_attribute_name = after_argument_list[1] + after_node_resource = self._get_node_resource_for( + resource_name=after_logical_name_of_resource, node_template=self._node_template ) - - # TODO: add support for this being created or removed. - before_outcome_unit = _compute_unit_for_if_statement(arguments_unit.before_context) - before_context = before_outcome_unit.before_context - after_outcome_unit = _compute_unit_for_if_statement(arguments_unit.after_context) - after_context = after_outcome_unit.after_context - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_intrinsic_function_fn_not( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - # TODO: add type checking/validation for result unit? - arguments_unit = self.visit(node_intrinsic_function.arguments) - before_condition = arguments_unit.before_context - after_condition = arguments_unit.after_context - if before_condition: - before_condition_outcome = before_condition[0] - before_context = not before_condition_outcome - else: - before_context = None - - if after_condition: - after_condition_outcome = after_condition[0] - after_context = not after_condition_outcome - else: - after_context = None - # Implicit change type computation. - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_intrinsic_function_fn_find_in_map( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. - # TODO: add type checking/validation for result unit? - arguments_unit = self.visit(node_intrinsic_function.arguments) - before_arguments = arguments_unit.before_context - after_arguments = arguments_unit.after_context - if before_arguments: - before_value_unit = self._resolve_mapping(*before_arguments) - before_context = before_value_unit.before_context - else: - before_context = None - if after_arguments: - after_value_unit = self._resolve_mapping(*after_arguments) - after_context = after_value_unit.after_context - else: - after_context = None - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_mapping(self, node_mapping: NodeMapping) -> DescribeUnit: - bindings_unit = self.visit(node_mapping.bindings) - return bindings_unit - - def visit_node_parameter(self, node_parameter: NodeParameter) -> DescribeUnit: - # TODO: add support for default value sampling - dynamic_value = node_parameter.dynamic_value - describe_unit = self.visit(dynamic_value) - return describe_unit - - def visit_node_condition(self, node_condition: NodeCondition) -> DescribeUnit: - describe_unit = self.visit(node_condition.body) - return describe_unit - - def visit_node_intrinsic_function_ref( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - arguments_unit = self.visit(node_intrinsic_function.arguments) - - # TODO: add tests with created and deleted parameters and verify this logic holds. - before_logical_id = arguments_unit.before_context - before_context = None - if before_logical_id is not None: - before_unit = self._resolve_reference(logica_id=before_logical_id) - before_context = before_unit.before_context - - after_logical_id = arguments_unit.after_context - after_context = None - if after_logical_id is not None: - after_unit = self._resolve_reference(logica_id=after_logical_id) - after_context = after_unit.after_context - - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_array(self, node_array: NodeArray) -> DescribeUnit: - before_context = list() - after_context = list() - for change_set_entity in node_array.array: - describe_unit: DescribeUnit = self.visit(change_set_entity=change_set_entity) - match change_set_entity.change_type: - case ChangeType.MODIFIED: - before_context.append(describe_unit.before_context) - after_context.append(describe_unit.after_context) - case ChangeType.CREATED: - after_context.append(describe_unit.after_context) - case ChangeType.REMOVED: - before_context.append(describe_unit.before_context) - case ChangeType.UNCHANGED: - before_context.append(describe_unit.before_context) - after_context.append(describe_unit.before_context) - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_properties(self, node_properties: NodeProperties) -> DescribeUnit: - before_context: dict[str, Any] = dict() - after_context: dict[str, Any] = dict() - for node_property in node_properties.properties: - describe_unit = self.visit(node_property.value) - property_name = node_property.name - match node_property.change_type: - case ChangeType.MODIFIED: - before_context[property_name] = describe_unit.before_context - after_context[property_name] = describe_unit.after_context - case ChangeType.CREATED: - after_context[property_name] = describe_unit.after_context - case ChangeType.REMOVED: - before_context[property_name] = describe_unit.before_context - case ChangeType.UNCHANGED: - before_context[property_name] = describe_unit.before_context - after_context[property_name] = describe_unit.before_context - # TODO: this object can probably be well-typed instead of a free dict(?) - before_context = {PropertiesKey: before_context} - after_context = {PropertiesKey: after_context} - return DescribeUnit(before_context=before_context, after_context=after_context) - - def _resolve_resource_condition_reference(self, reference: TerminalValue) -> DescribeUnit: - reference_unit = self.visit(reference) - before_reference = reference_unit.before_context - after_reference = reference_unit.after_context - condition_unit = self._resolve_reference_binding( - before_logical_id=before_reference, after_logical_id=after_reference - ) - before_context = ( - condition_unit.before_context if not isinstance(before_reference, NothingType) else True - ) - after_context = ( - condition_unit.after_context if not isinstance(after_reference, NothingType) else True - ) - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_output(self, node_output: NodeOutput) -> DescribeUnit: - # This logic is not required for Describe operations, - # and should be ported a new base for this class type. - change_type = node_output.change_type - value_unit = self.visit(node_output.value) - - condition_unit = None - if node_output.condition_reference is not None: - condition_unit = self._resolve_resource_condition_reference( - node_output.condition_reference + after_node_property = self._get_node_property_for( + property_name=after_attribute_name, node_resource=after_node_resource ) - condition_before = condition_unit.before_context - condition_after = condition_unit.after_context - if not condition_before and condition_after: - change_type = ChangeType.CREATED - elif condition_before and not condition_after: - change_type = ChangeType.REMOVED + after_property_delta = self.visit(after_node_property) + if after_property_delta.before == after_property_delta.after: + after = after_property_delta.after + else: + after = CHANGESET_KNOWN_AFTER_APPLY - export_unit = None - if node_output.export is not None: - export_unit = self.visit(node_output.export) + return PreprocEntityDelta(before=before, after=after) - before_context = None - after_context = None - if change_type != ChangeType.REMOVED: - after_context = {"Name": node_output.name, ValueKey: value_unit.after_context} - if export_unit: - after_context[ExportKey] = export_unit.after_context - if condition_unit: - after_context[ConditionKey] = condition_unit.after_context - if change_type != ChangeType.CREATED: - before_context = {"Name": node_output.name, ValueKey: value_unit.before_context} - if export_unit: - before_context[ExportKey] = export_unit.before_context - if condition_unit: - before_context[ConditionKey] = condition_unit.before_context - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_outputs(self, node_outputs: NodeOutputs) -> DescribeUnit: - # This logic is not required for Describe operations, - # and should be ported a new base for this class type. - before_context = list() - after_context = list() - for node_output in node_outputs.outputs: - output_unit = self.visit(node_output) - output_before = output_unit.before_context - output_after = output_unit.after_context - if output_before: - before_context.append(output_before) - if output_after: - after_context.append(output_after) - return DescribeUnit(before_context=before_context, after_context=after_context) - - def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: - change_type = node_resource.change_type - if node_resource.condition_reference is not None: - condition_unit = self._resolve_resource_condition_reference( - node_resource.condition_reference - ) - condition_before = condition_unit.before_context - condition_after = condition_unit.after_context - if not condition_before and condition_after: - change_type = ChangeType.CREATED - elif condition_before and not condition_after: - change_type = ChangeType.REMOVED + def _register_resource_change( + self, + logical_id: str, + type_: str, + before_properties: Optional[PreprocProperties], + after_properties: Optional[PreprocProperties], + ) -> None: + # unchanged: nothing to do. + if before_properties == after_properties: + return + + action = cfn_api.ChangeAction.Modify + if before_properties is None: + action = cfn_api.ChangeAction.Add + elif after_properties is None: + action = cfn_api.ChangeAction.Remove resource_change = cfn_api.ResourceChange() - resource_change["LogicalResourceId"] = node_resource.name - - # TODO: investigate effects on type changes - type_describe_unit = self.visit(node_resource.type_) - resource_change["ResourceType"] = ( - type_describe_unit.before_context or type_describe_unit.after_context + resource_change["Action"] = action + resource_change["LogicalResourceId"] = logical_id + resource_change["ResourceType"] = type_ + if self._include_property_values and before_properties is not None: + before_context_properties = {PropertiesKey: before_properties.properties} + before_context_properties_json_str = json.dumps(before_context_properties) + resource_change["BeforeContext"] = before_context_properties_json_str + if self._include_property_values and after_properties is not None: + after_context_properties = {PropertiesKey: after_properties.properties} + after_context_properties_json_str = json.dumps(after_context_properties) + resource_change["AfterContext"] = after_context_properties_json_str + self._changes.append( + cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change) ) - properties_describe_unit = self.visit(node_resource.properties) - - if change_type != ChangeType.UNCHANGED: - match change_type: - case ChangeType.MODIFIED: - resource_change["Action"] = cfn_api.ChangeAction.Modify - resource_change["BeforeContext"] = properties_describe_unit.before_context - resource_change["AfterContext"] = properties_describe_unit.after_context - case ChangeType.CREATED: - resource_change["Action"] = cfn_api.ChangeAction.Add - resource_change["AfterContext"] = properties_describe_unit.after_context - case ChangeType.REMOVED: - resource_change["Action"] = cfn_api.ChangeAction.Remove - resource_change["BeforeContext"] = properties_describe_unit.before_context - self._changes.append( - cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change) + def _describe_resource_change( + self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] + ) -> None: + if before is not None and after is not None: + # Case: change on same type. + if before.resource_type == after.resource_type: + # Register a Modified if changed. + self._register_resource_change( + logical_id=name, + type_=before.resource_type, + before_properties=before.properties, + after_properties=after.properties, + ) + # Case: type migration. + # TODO: Add test to assert that on type change the resources are replaced. + else: + # Register a Removed for the previous type. + self._register_resource_change( + logical_id=name, + type_=before.resource_type, + before_properties=before.properties, + after_properties=None, + ) + # Register a Create for the next type. + self._register_resource_change( + logical_id=name, + type_=after.resource_type, + before_properties=None, + after_properties=after.properties, + ) + elif before is not None: + # Case: removal + self._register_resource_change( + logical_id=name, + type_=before.resource_type, + before_properties=before.properties, + after_properties=None, + ) + elif after is not None: + # Case: addition + self._register_resource_change( + logical_id=name, + type_=after.resource_type, + before_properties=None, + after_properties=after.properties, ) - before_context = None - after_context = None - # TODO: reconsider what is the describe unit return value for a resource type. - if change_type != ChangeType.CREATED: - before_context = node_resource.name - if change_type != ChangeType.REMOVED: - after_context = node_resource.name - return DescribeUnit(before_context=before_context, after_context=after_context) + def visit_node_resource( + self, node_resource: NodeResource + ) -> PreprocEntityDelta[PreprocResource, PreprocResource]: + delta = super().visit_node_resource(node_resource=node_resource) + self._describe_resource_change( + name=node_resource.name, before=delta.before, after=delta.after + ) + return delta diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index cd162e9c77d57..60160ef221431 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -1,18 +1,18 @@ import logging import uuid -from typing import Final +from typing import Any, Final, Optional from localstack.aws.api.cloudformation import ChangeAction from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY from localstack.services.cloudformation.engine.v2.change_set_model import ( - NodeIntrinsicFunction, NodeResource, NodeTemplate, - TerminalValue, ) -from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( - ChangeSetModelDescriber, - DescribeUnit, +from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( + ChangeSetModelPreproc, + PreprocEntityDelta, + PreprocProperties, + PreprocResource, ) from localstack.services.cloudformation.resource_provider import ( Credentials, @@ -26,7 +26,7 @@ LOG = logging.getLogger(__name__) -class ChangeSetModelExecutor(ChangeSetModelDescriber): +class ChangeSetModelExecutor(ChangeSetModelPreproc): account_id: Final[str] region: Final[str] @@ -46,31 +46,94 @@ def __init__( self.resources = {} def execute(self) -> dict: - self.visit(self._node_template) + self.process() return self.resources - def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: - resource_provider_executor = ResourceProviderExecutor( - stack_name=self.stack_name, stack_id=self.stack_id + def visit_node_resource( + self, node_resource: NodeResource + ) -> PreprocEntityDelta[PreprocResource, PreprocResource]: + delta = super().visit_node_resource(node_resource=node_resource) + self._execute_on_resource_change( + name=node_resource.name, before=delta.before, after=delta.after ) - - # TODO: investigate effects on type changes - properties_describe_unit = self.visit_node_properties(node_resource.properties) - LOG.info("SRW: describe unit: %s", properties_describe_unit) - - action = node_resource.change_type.to_action() - if action is None: - raise RuntimeError( - f"Action should always be present, got change type: {node_resource.change_type}" + return delta + + def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> Any: + # TODO: this should be implemented to compute the runtime reference value for node entities. + return super()._reduce_intrinsic_function_ref_value(preproc_value=preproc_value) + + def _execute_on_resource_change( + self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] + ) -> None: + # TODO: this logic is a POC and should be revised. + if before is not None and after is not None: + # Case: change on same type. + if before.resource_type == after.resource_type: + # Register a Modified if changed. + self._execute_resource_action( + action=ChangeAction.Modify, + logical_resource_id=name, + resource_type=before.resource_type, + before_properties=before.properties, + after_properties=after.properties, + ) + # Case: type migration. + # TODO: Add test to assert that on type change the resources are replaced. + else: + # Register a Removed for the previous type. + self._execute_resource_action( + action=ChangeAction.Remove, + logical_resource_id=name, + resource_type=before.resource_type, + before_properties=before.properties, + after_properties=None, + ) + # Register a Create for the next type. + self._execute_resource_action( + action=ChangeAction.Add, + logical_resource_id=name, + resource_type=after.resource_type, + before_properties=None, + after_properties=after.properties, + ) + elif before is not None: + # Case: removal + self._execute_resource_action( + action=ChangeAction.Remove, + logical_resource_id=name, + resource_type=before.resource_type, + before_properties=before.properties, + after_properties=None, + ) + elif after is not None: + # Case: addition + self._execute_resource_action( + action=ChangeAction.Add, + logical_resource_id=name, + resource_type=after.resource_type, + before_properties=None, + after_properties=after.properties, ) + def _execute_resource_action( + self, + action: ChangeAction, + logical_resource_id: str, + resource_type: str, + before_properties: Optional[PreprocProperties], + after_properties: Optional[PreprocProperties], + ) -> None: + resource_provider_executor = ResourceProviderExecutor( + stack_name=self.stack_name, stack_id=self.stack_id + ) # TODO - resource_type = get_resource_type({"Type": "AWS::SSM::Parameter"}) + resource_type = get_resource_type({"Type": resource_type}) payload = self.create_resource_provider_payload( - properties_describe_unit, - action, - node_resource.name, - resource_type, + action=action, + logical_resource_id=logical_resource_id, + resource_type=resource_type, + before_properties=before_properties, + after_properties=after_properties, ) resource_provider = resource_provider_executor.try_load_resource_provider(resource_type) @@ -83,68 +146,41 @@ def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit: else: event = ProgressEvent(OperationStatus.SUCCESS, resource_model={}) - self.resources.setdefault(node_resource.name, {"Properties": {}}) + self.resources.setdefault(logical_resource_id, {"Properties": {}}) match event.status: case OperationStatus.SUCCESS: # merge the resources state with the external state # TODO: this is likely a duplicate of updating from extra_resource_properties - self.resources[node_resource.name]["Properties"].update(event.resource_model) - self.resources[node_resource.name].update(extra_resource_properties) + self.resources[logical_resource_id]["Properties"].update(event.resource_model) + self.resources[logical_resource_id].update(extra_resource_properties) # XXX for legacy delete_stack compatibility - self.resources[node_resource.name]["LogicalResourceId"] = node_resource.name - self.resources[node_resource.name]["Type"] = resource_type + self.resources[logical_resource_id]["LogicalResourceId"] = logical_resource_id + self.resources[logical_resource_id]["Type"] = resource_type case any: raise NotImplementedError(f"Event status '{any}' not handled") - return DescribeUnit(before_context=None, after_context={}) - - def visit_node_intrinsic_function_fn_get_att( - self, node_intrinsic_function: NodeIntrinsicFunction - ) -> DescribeUnit: - arguments_unit = self.visit(node_intrinsic_function.arguments) - before_arguments_list = arguments_unit.before_context - after_arguments_list = arguments_unit.after_context - if before_arguments_list: - logical_name_of_resource = before_arguments_list[0] - attribute_name = before_arguments_list[1] - before_node_resource = self._get_node_resource_for( - resource_name=logical_name_of_resource, node_template=self._node_template - ) - node_property: TerminalValue = self._get_node_property_for( - property_name=attribute_name, node_resource=before_node_resource - ) - before_context = self.visit(node_property.value).before_context - else: - before_context = None - - if after_arguments_list: - logical_name_of_resource = after_arguments_list[0] - attribute_name = after_arguments_list[1] - after_node_resource = self._get_node_resource_for( - resource_name=logical_name_of_resource, node_template=self._node_template - ) - node_property: TerminalValue = self._get_node_property_for( - property_name=attribute_name, node_resource=after_node_resource - ) - after_context = self.visit(node_property.value).after_context - else: - after_context = None - - return DescribeUnit(before_context=before_context, after_context=after_context) - def create_resource_provider_payload( self, - describe_unit: DescribeUnit, action: ChangeAction, logical_resource_id: str, resource_type: str, - ) -> ResourceProviderPayload: + before_properties: Optional[PreprocProperties], + after_properties: Optional[PreprocProperties], + ) -> Optional[ResourceProviderPayload]: # FIXME: use proper credentials creds: Credentials = { "accessKeyId": self.account_id, "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY, "sessionToken": "", } + before_properties_value = before_properties.properties if before_properties else None + if action == ChangeAction.Remove: + resource_properties = before_properties_value + previous_resource_properties = None + else: + after_properties_value = after_properties.properties if after_properties else None + resource_properties = after_properties_value + previous_resource_properties = before_properties_value resource_provider_payload: ResourceProviderPayload = { "awsAccountId": self.account_id, "callbackContext": {}, @@ -157,8 +193,9 @@ def create_resource_provider_payload( "action": str(action), "requestData": { "logicalResourceId": logical_resource_id, - "resourceProperties": describe_unit.after_context["Properties"], - "previousResourceProperties": describe_unit.before_context["Properties"], + # TODO: assign before and previous according on the action type. + "resourceProperties": resource_properties, + "previousResourceProperties": previous_resource_properties, "callerCredentials": creds, "providerCredentials": creds, "systemTags": {}, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py new file mode 100644 index 0000000000000..bc3e6ce07beb9 --- /dev/null +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -0,0 +1,574 @@ +from __future__ import annotations + +from typing import Any, Final, Generic, Optional, TypeVar + +from localstack.services.cloudformation.engine.v2.change_set_model import ( + ChangeSetEntity, + ChangeType, + NodeArray, + NodeCondition, + NodeDivergence, + NodeIntrinsicFunction, + NodeMapping, + NodeObject, + NodeOutput, + NodeOutputs, + NodeParameter, + NodeProperties, + NodeProperty, + NodeResource, + NodeTemplate, + NothingType, + Scope, + TerminalValue, + TerminalValueCreated, + TerminalValueModified, + TerminalValueRemoved, + TerminalValueUnchanged, +) +from localstack.services.cloudformation.engine.v2.change_set_model_visitor import ( + ChangeSetModelVisitor, +) + +TBefore = TypeVar("TBefore") +TAfter = TypeVar("TAfter") + + +class PreprocEntityDelta(Generic[TBefore, TAfter]): + before: Optional[TBefore] + after: Optional[TAfter] + + def __init__(self, before: Optional[TBefore] = None, after: Optional[TAfter] = None): + self.before = before + self.after = after + + def __eq__(self, other): + if not isinstance(other, PreprocEntityDelta): + return False + return self.before == other.before and self.after == other.after + + +class PreprocProperties: + properties: dict[str, Any] + + def __init__(self, properties: dict[str, Any]): + self.properties = properties + + def __eq__(self, other): + if not isinstance(other, PreprocProperties): + return False + return self.properties == other.properties + + +class PreprocResource: + name: str + condition: Optional[bool] + resource_type: str + properties: PreprocProperties + + def __init__( + self, + name: str, + condition: Optional[bool], + resource_type: str, + properties: PreprocProperties, + ): + self.condition = condition + self.name = name + self.resource_type = resource_type + self.properties = properties + + def __eq__(self, other): + if not isinstance(other, PreprocResource): + return False + return all( + [ + self.name == other.name, + self.condition == other.condition, + self.resource_type == other.resource_type, + self.properties == other.properties, + ] + ) + + +class PreprocOutput: + name: str + value: Any + export: Optional[Any] + condition: Optional[bool] + + def __init__(self, name: str, value: Any, export: Optional[Any], condition: Optional[bool]): + self.name = name + self.value = value + self.export = export + self.condition = condition + + def __eq__(self, other): + if not isinstance(other, PreprocOutput): + return False + return all( + [ + self.name == other.name, + self.value == other.value, + self.export == other.export, + self.condition == other.condition, + ] + ) + + +class ChangeSetModelPreproc(ChangeSetModelVisitor): + _node_template: Final[NodeTemplate] + _processed: dict[Scope, Any] + + def __init__(self, node_template: NodeTemplate): + self._node_template = node_template + self._processed = dict() + + def process(self) -> None: + self._processed.clear() + self.visit(self._node_template) + + def _get_node_resource_for( + self, resource_name: str, node_template: NodeTemplate + ) -> NodeResource: + # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists. + for node_resource in node_template.resources.resources: + if node_resource.name == resource_name: + return node_resource + # TODO + raise RuntimeError() + + def _get_node_property_for( + self, property_name: str, node_resource: NodeResource + ) -> NodeProperty: + # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists. + for node_property in node_resource.properties.properties: + if node_property.name == property_name: + return node_property + # TODO + raise RuntimeError() + + def _get_node_mapping(self, map_name: str) -> NodeMapping: + mappings: list[NodeMapping] = self._node_template.mappings.mappings + # TODO: another scenarios suggesting property lookups might be preferable. + for mapping in mappings: + if mapping.name == map_name: + return mapping + # TODO + raise RuntimeError() + + def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]: + parameters: list[NodeParameter] = self._node_template.parameters.parameters + # TODO: another scenarios suggesting property lookups might be preferable. + for parameter in parameters: + if parameter.name == parameter_name: + return parameter + return None + + def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]: + conditions: list[NodeCondition] = self._node_template.conditions.conditions + # TODO: another scenarios suggesting property lookups might be preferable. + for condition in conditions: + if condition.name == condition_name: + return condition + return None + + def _resolve_reference(self, logica_id: str) -> PreprocEntityDelta: + node_condition = self._get_node_condition_if_exists(condition_name=logica_id) + if isinstance(node_condition, NodeCondition): + condition_delta = self.visit(node_condition) + return condition_delta + + node_parameter = self._get_node_parameter_if_exists(parameter_name=logica_id) + if isinstance(node_parameter, NodeParameter): + parameter_delta = self.visit(node_parameter) + return parameter_delta + + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + node_resource = self._get_node_resource_for( + resource_name=logica_id, node_template=self._node_template + ) + resource_delta = self.visit(node_resource) + before = resource_delta.before + after = resource_delta.after + return PreprocEntityDelta(before=before, after=after) + + def _resolve_mapping( + self, map_name: str, top_level_key: str, second_level_key + ) -> PreprocEntityDelta: + # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids. + node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name) + top_level_value = node_mapping.bindings.bindings.get(top_level_key) + if not isinstance(top_level_value, NodeObject): + raise RuntimeError() + second_level_value = top_level_value.bindings.get(second_level_key) + mapping_value_delta = self.visit(second_level_value) + return mapping_value_delta + + def _resolve_reference_binding( + self, before_logical_id: Optional[str], after_logical_id: Optional[str] + ) -> PreprocEntityDelta: + before = None + if before_logical_id is not None: + before_delta = self._resolve_reference(logica_id=before_logical_id) + before = before_delta.before + after = None + if after_logical_id is not None: + after_delta = self._resolve_reference(logica_id=after_logical_id) + after = after_delta.after + return PreprocEntityDelta(before=before, after=after) + + def visit(self, change_set_entity: ChangeSetEntity) -> PreprocEntityDelta: + delta = self._processed.get(change_set_entity.scope) + if delta is not None: + return delta + delta = super().visit(change_set_entity=change_set_entity) + self._processed[change_set_entity.scope] = delta + return delta + + def visit_terminal_value_modified( + self, terminal_value_modified: TerminalValueModified + ) -> PreprocEntityDelta: + return PreprocEntityDelta( + before=terminal_value_modified.value, + after=terminal_value_modified.modified_value, + ) + + def visit_terminal_value_created( + self, terminal_value_created: TerminalValueCreated + ) -> PreprocEntityDelta: + return PreprocEntityDelta(after=terminal_value_created.value) + + def visit_terminal_value_removed( + self, terminal_value_removed: TerminalValueRemoved + ) -> PreprocEntityDelta: + return PreprocEntityDelta(before=terminal_value_removed.value) + + def visit_terminal_value_unchanged( + self, terminal_value_unchanged: TerminalValueUnchanged + ) -> PreprocEntityDelta: + return PreprocEntityDelta( + before=terminal_value_unchanged.value, + after=terminal_value_unchanged.value, + ) + + def visit_node_divergence(self, node_divergence: NodeDivergence) -> PreprocEntityDelta: + before_delta = self.visit(node_divergence.value) + after_delta = self.visit(node_divergence.divergence) + return PreprocEntityDelta(before=before_delta.before, after=after_delta.after) + + def visit_node_object(self, node_object: NodeObject) -> PreprocEntityDelta: + before = dict() + after = dict() + for name, change_set_entity in node_object.bindings.items(): + delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity) + match change_set_entity.change_type: + case ChangeType.MODIFIED: + before[name] = delta.before + after[name] = delta.after + case ChangeType.CREATED: + after[name] = delta.after + case ChangeType.REMOVED: + before[name] = delta.before + case ChangeType.UNCHANGED: + before[name] = delta.before + after[name] = delta.before + return PreprocEntityDelta(before=before, after=after) + + def visit_node_intrinsic_function_fn_get_att( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + arguments_delta = self.visit(node_intrinsic_function.arguments) + # TODO: validate the return value according to the spec. + before_argument_list = arguments_delta.before + after_argument_list = arguments_delta.after + + before = None + if before_argument_list: + before_logical_name_of_resource = before_argument_list[0] + before_attribute_name = before_argument_list[1] + before_node_resource = self._get_node_resource_for( + resource_name=before_logical_name_of_resource, node_template=self._node_template + ) + before_node_property = self._get_node_property_for( + property_name=before_attribute_name, node_resource=before_node_resource + ) + before_property_delta = self.visit(before_node_property) + before = before_property_delta.before + + after = None + if after_argument_list: + # TODO: when are values only accessible at runtime? + after_logical_name_of_resource = after_argument_list[0] + after_attribute_name = after_argument_list[1] + after_node_resource = self._get_node_resource_for( + resource_name=after_logical_name_of_resource, node_template=self._node_template + ) + after_node_property = self._get_node_property_for( + property_name=after_attribute_name, node_resource=after_node_resource + ) + after_property_delta = self.visit(after_node_property) + after = after_property_delta.after + + return PreprocEntityDelta(before=before, after=after) + + def visit_node_intrinsic_function_fn_equals( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + arguments_delta = self.visit(node_intrinsic_function.arguments) + before_values = arguments_delta.before + after_values = arguments_delta.after + before = None + if before_values: + before = before_values[0] == before_values[1] + after = None + if after_values: + after = after_values[0] == after_values[1] + return PreprocEntityDelta(before=before, after=after) + + def visit_node_intrinsic_function_fn_if( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + arguments_delta = self.visit(node_intrinsic_function.arguments) + + def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta: + condition_name = args[0] + boolean_expression_delta = self._resolve_reference(logica_id=condition_name) + return PreprocEntityDelta( + before=args[1] if boolean_expression_delta.before else args[2], + after=args[1] if boolean_expression_delta.after else args[2], + ) + + # TODO: add support for this being created or removed. + before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before) + before = before_outcome_delta.before + after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after) + after = after_outcome_delta.after + return PreprocEntityDelta(before=before, after=after) + + def visit_node_intrinsic_function_fn_not( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + # TODO: add type checking/validation for result unit? + arguments_delta = self.visit(node_intrinsic_function.arguments) + before_condition = arguments_delta.before + after_condition = arguments_delta.after + if before_condition: + before_condition_outcome = before_condition[0] + before = not before_condition_outcome + else: + before = None + + if after_condition: + after_condition_outcome = after_condition[0] + after = not after_condition_outcome + else: + after = None + # Implicit change type computation. + return PreprocEntityDelta(before=before, after=after) + + def visit_node_intrinsic_function_fn_find_in_map( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. + # TODO: add type checking/validation for result unit? + arguments_delta = self.visit(node_intrinsic_function.arguments) + before_arguments = arguments_delta.before + after_arguments = arguments_delta.after + if before_arguments: + before_value_delta = self._resolve_mapping(*before_arguments) + before = before_value_delta.before + else: + before = None + if after_arguments: + after_value_delta = self._resolve_mapping(*after_arguments) + after = after_value_delta.after + else: + after = None + return PreprocEntityDelta(before=before, after=after) + + def visit_node_mapping(self, node_mapping: NodeMapping) -> PreprocEntityDelta: + bindings_delta = self.visit(node_mapping.bindings) + return bindings_delta + + def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta: + # TODO: add support for default value sampling + dynamic_value = node_parameter.dynamic_value + delta = self.visit(dynamic_value) + return delta + + def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDelta: + delta = self.visit(node_condition.body) + return delta + + def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> Any: + if isinstance(preproc_value, PreprocResource): + value = preproc_value.name + else: + value = preproc_value + return value + + def visit_node_intrinsic_function_ref( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + arguments_delta = self.visit(node_intrinsic_function.arguments) + + # TODO: add tests with created and deleted parameters and verify this logic holds. + before_logical_id = arguments_delta.before + before = None + if before_logical_id is not None: + before_delta = self._resolve_reference(logica_id=before_logical_id) + before_value = before_delta.before + before = self._reduce_intrinsic_function_ref_value(before_value) + + after_logical_id = arguments_delta.after + after = None + if after_logical_id is not None: + after_delta = self._resolve_reference(logica_id=after_logical_id) + after_value = after_delta.after + after = self._reduce_intrinsic_function_ref_value(after_value) + + return PreprocEntityDelta(before=before, after=after) + + def visit_node_array(self, node_array: NodeArray) -> PreprocEntityDelta: + before = list() + after = list() + for change_set_entity in node_array.array: + delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity) + if delta.before: + before.append(delta.before) + if delta.after: + after.append(delta.after) + return PreprocEntityDelta(before=before, after=after) + + def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta: + return self.visit(node_property.value) + + def visit_node_properties( + self, node_properties: NodeProperties + ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]: + before_bindings: dict[str, Any] = dict() + after_bindings: dict[str, Any] = dict() + for node_property in node_properties.properties: + delta = self.visit(node_property) + property_name = node_property.name + if node_property.change_type != ChangeType.CREATED: + before_bindings[property_name] = delta.before + if node_property.change_type != ChangeType.REMOVED: + after_bindings[property_name] = delta.after + before = None + if before_bindings: + before = PreprocProperties(properties=before_bindings) + after = None + if after_bindings: + after = PreprocProperties(properties=after_bindings) + return PreprocEntityDelta(before=before, after=after) + + def _resolve_resource_condition_reference(self, reference: TerminalValue) -> PreprocEntityDelta: + reference_delta = self.visit(reference) + before_reference = reference_delta.before + after_reference = reference_delta.after + condition_delta = self._resolve_reference_binding( + before_logical_id=before_reference, after_logical_id=after_reference + ) + before = condition_delta.before if not isinstance(before_reference, NothingType) else True + after = condition_delta.after if not isinstance(after_reference, NothingType) else True + return PreprocEntityDelta(before=before, after=after) + + def visit_node_resource( + self, node_resource: NodeResource + ) -> PreprocEntityDelta[PreprocResource, PreprocResource]: + change_type = node_resource.change_type + condition_before = None + condition_after = None + if node_resource.condition_reference is not None: + condition_delta = self._resolve_resource_condition_reference( + node_resource.condition_reference + ) + condition_before = condition_delta.before + condition_after = condition_delta.after + if not condition_before and condition_after: + change_type = ChangeType.CREATED + elif condition_before and not condition_after: + change_type = ChangeType.REMOVED + + type_delta = self.visit(node_resource.type_) + properties_delta: PreprocEntityDelta[PreprocProperties, PreprocProperties] = self.visit( + node_resource.properties + ) + + before = None + after = None + if change_type != ChangeType.CREATED: + before = PreprocResource( + name=node_resource.name, + condition=condition_before, + resource_type=type_delta.before, + properties=properties_delta.before, + ) + if change_type != ChangeType.REMOVED: + after = PreprocResource( + name=node_resource.name, + condition=condition_after, + resource_type=type_delta.after, + properties=properties_delta.after, + ) + return PreprocEntityDelta(before=before, after=after) + + def visit_node_output( + self, node_output: NodeOutput + ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]: + change_type = node_output.change_type + value_delta = self.visit(node_output.value) + + condition_delta = None + if node_output.condition_reference is not None: + condition_delta = self._resolve_resource_condition_reference( + node_output.condition_reference + ) + condition_before = condition_delta.before + condition_after = condition_delta.after + if not condition_before and condition_after: + change_type = ChangeType.CREATED + elif condition_before and not condition_after: + change_type = ChangeType.REMOVED + + export_delta = None + if node_output.export is not None: + export_delta = self.visit(node_output.export) + + before: Optional[PreprocOutput] = None + if change_type != ChangeType.CREATED: + before = PreprocOutput( + name=node_output.name, + value=value_delta.before, + export=export_delta.before if export_delta else None, + condition=condition_delta.before if condition_delta else None, + ) + after: Optional[PreprocOutput] = None + if change_type != ChangeType.REMOVED: + after = PreprocOutput( + name=node_output.name, + value=value_delta.after, + export=export_delta.after if export_delta else None, + condition=condition_delta.after if condition_delta else None, + ) + return PreprocEntityDelta(before=before, after=after) + + def visit_node_outputs( + self, node_outputs: NodeOutputs + ) -> PreprocEntityDelta[list[PreprocOutput], list[PreprocOutput]]: + before: list[PreprocOutput] = list() + after: list[PreprocOutput] = list() + for node_output in node_outputs.outputs: + output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output) + output_before = output_delta.before + output_after = output_delta.after + if output_before: + before.append(output_before) + if output_after: + after.append(output_after) + return PreprocEntityDelta(before=before, after=after) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py index c7340cac44c4b..80b93b820f8de 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py @@ -49,18 +49,18 @@ def visit_children(self, change_set_entity: ChangeSetEntity): def visit_node_template(self, node_template: NodeTemplate): self.visit_children(node_template) - def visit_node_mapping(self, node_mapping: NodeMapping): - self.visit_children(node_mapping) - - def visit_node_mappings(self, node_mappings: NodeMappings): - self.visit_children(node_mappings) - def visit_node_outputs(self, node_outputs: NodeOutputs): self.visit_children(node_outputs) def visit_node_output(self, node_output: NodeOutput): self.visit_children(node_output) + def visit_node_mapping(self, node_mapping: NodeMapping): + self.visit_children(node_mapping) + + def visit_node_mappings(self, node_mappings: NodeMappings): + self.visit_children(node_mappings) + def visit_node_parameters(self, node_parameters: NodeParameters): self.visit_children(node_parameters) diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index b713da7ffe670..0dfa5ec52b297 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -1,8 +1,11 @@ +import json import logging from copy import deepcopy +from typing import Any from localstack.aws.api import RequestContext, handler from localstack.aws.api.cloudformation import ( + Changes, ChangeSetNameOrId, ChangeSetNotFoundException, ChangeSetType, @@ -188,6 +191,30 @@ def create_change_set( resolved_parameters=resolved_parameters, ) + # TODO: reconsider the way parameters are modelled in the update graph process. + # The options might be reduce to using the current style, or passing the extra information + # as a metadata object. The choice should be made considering when the extra information + # is needed for the update graph building, or only looked up in downstream tasks (metadata). + request_parameters = request.get("Parameters", list()) + after_parameters: dict[str, Any] = { + parameter["ParameterKey"]: parameter["ParameterValue"] + for parameter in request_parameters + } + before_parameters: dict[str, Any] = { + parameter["ParameterKey"]: parameter["ParameterValue"] + for parameter in old_parameters.values() + } + + # TODO: update this logic to always pass the clean template object if one exists. The + # current issue with relaying on stack.template_original is that this appears to have + # its parameters and conditions populated. + before_template = None + if change_set_type == ChangeSetType.UPDATE: + before_template = json.loads( + stack.template_body + ) # template_original is sometimes invalid + after_template = template + # create change set for the stack and apply changes change_set = StackChangeSet( context.account_id, @@ -199,9 +226,14 @@ def create_change_set( ) # only set parameters for the changeset, then switch to stack on execute_change_set change_set.template_body = template_body - change_set.populate_update_graph(stack.template, transformed_template) + change_set.populate_update_graph( + before_template=before_template, + after_template=after_template, + before_parameters=before_parameters, + after_parameters=after_parameters, + ) - # TODO: evaluate conditions + # TODO: move this logic of condition resolution with metadata to the ChangeSetModelPreproc or Executor raw_conditions = transformed_template.get("Conditions", {}) resolved_stack_conditions = resolve_stack_conditions( account_id=context.account_id, @@ -212,6 +244,7 @@ def create_change_set( stack_name=stack_name, ) change_set.set_resolved_stack_conditions(resolved_stack_conditions) + change_set.set_resolved_parameters(resolved_parameters) # a bit gross but use the template ordering to validate missing resources try: @@ -326,9 +359,10 @@ def describe_change_set( raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") change_set_describer = ChangeSetModelDescriber( - node_template=change_set.update_graph, include_property_values=include_property_values + node_template=change_set.update_graph, + include_property_values=bool(include_property_values), ) - resource_changes = change_set_describer.get_changes() + changes: Changes = change_set_describer.get_changes() attrs = [ "ChangeSetType", @@ -343,5 +377,5 @@ def describe_change_set( result["Parameters"] = [ mask_no_echo(strip_parameter_type(p)) for p in result.get("Parameters", []) ] - result["Changes"] = resource_changes + result["Changes"] = changes return result diff --git a/localstack-core/localstack/services/cloudformation/v2/utils.py b/localstack-core/localstack/services/cloudformation/v2/utils.py new file mode 100644 index 0000000000000..02a6cbb971a99 --- /dev/null +++ b/localstack-core/localstack/services/cloudformation/v2/utils.py @@ -0,0 +1,5 @@ +from localstack import config + + +def is_v2_engine() -> bool: + return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2" diff --git a/localstack-core/localstack/services/sns/resource_providers/aws_sns_topic.py b/localstack-core/localstack/services/sns/resource_providers/aws_sns_topic.py index 165545353b0d1..00b68044ae750 100644 --- a/localstack-core/localstack/services/sns/resource_providers/aws_sns_topic.py +++ b/localstack-core/localstack/services/sns/resource_providers/aws_sns_topic.py @@ -148,6 +148,7 @@ def delete( IAM permissions required: - sns:DeleteTopic """ + # FIXME: This appears to incorrectly assume TopicArn would be provided. model = request.desired_state sns = request.aws_client_factory.sns sns.delete_topic(TopicArn=model["TopicArn"]) diff --git a/localstack-core/localstack/testing/pytest/cloudformation/__init__.py b/localstack-core/localstack/testing/pytest/cloudformation/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py b/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py new file mode 100644 index 0000000000000..e2c42d38076ca --- /dev/null +++ b/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py @@ -0,0 +1,169 @@ +import json +from collections import defaultdict +from typing import Callable + +import pytest + +from localstack.aws.api.cloudformation import StackEvent +from localstack.aws.connect import ServiceLevelClientFactory +from localstack.utils.functions import call_safe +from localstack.utils.strings import short_uid + +PerResourceStackEvents = dict[str, list[StackEvent]] + + +@pytest.fixture +def capture_per_resource_events( + aws_client: ServiceLevelClientFactory, +) -> Callable[[str], PerResourceStackEvents]: + def capture(stack_name: str) -> PerResourceStackEvents: + events = aws_client.cloudformation.describe_stack_events(StackName=stack_name)[ + "StackEvents" + ] + per_resource_events = defaultdict(list) + for event in events: + if logical_resource_id := event.get("LogicalResourceId"): + per_resource_events[logical_resource_id].append(event) + return per_resource_events + + return capture + + +@pytest.fixture +def capture_update_process(aws_client_no_retry, cleanups, capture_per_resource_events): + """ + Fixture to deploy a new stack (via creating and executing a change set), then updating the + stack with a second template (via creating and executing a change set). + """ + + stack_name = f"stack-{short_uid()}" + change_set_name = f"cs-{short_uid()}" + + def inner( + snapshot, t1: dict | str, t2: dict | str, p1: dict | None = None, p2: dict | None = None + ): + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + + if isinstance(t1, dict): + t1 = json.dumps(t1) + elif isinstance(t1, str): + with open(t1) as infile: + t1 = infile.read() + if isinstance(t2, dict): + t2 = json.dumps(t2) + elif isinstance(t2, str): + with open(t2) as infile: + t2 = infile.read() + + p1 = p1 or {} + p2 = p2 or {} + + # deploy original stack + change_set_details = aws_client_no_retry.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=t1, + ChangeSetType="CREATE", + Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p1.items()], + ) + snapshot.match("create-change-set-1", change_set_details) + stack_id = change_set_details["StackId"] + change_set_id = change_set_details["Id"] + aws_client_no_retry.cloudformation.get_waiter("change_set_create_complete").wait( + ChangeSetName=change_set_id + ) + cleanups.append( + lambda: call_safe( + aws_client_no_retry.cloudformation.delete_change_set, + kwargs=dict(ChangeSetName=change_set_id), + ) + ) + + describe_change_set_with_prop_values = ( + aws_client_no_retry.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=True + ) + ) + snapshot.match("describe-change-set-1-prop-values", describe_change_set_with_prop_values) + describe_change_set_without_prop_values = ( + aws_client_no_retry.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=False + ) + ) + snapshot.match("describe-change-set-1", describe_change_set_without_prop_values) + + execute_results = aws_client_no_retry.cloudformation.execute_change_set( + ChangeSetName=change_set_id + ) + snapshot.match("execute-change-set-1", execute_results) + aws_client_no_retry.cloudformation.get_waiter("stack_create_complete").wait( + StackName=stack_id + ) + + # ensure stack deletion + cleanups.append( + lambda: call_safe( + aws_client_no_retry.cloudformation.delete_stack, kwargs=dict(StackName=stack_id) + ) + ) + + describe = aws_client_no_retry.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][ + 0 + ] + snapshot.match("post-create-1-describe", describe) + + # update stack + change_set_details = aws_client_no_retry.cloudformation.create_change_set( + StackName=stack_name, + ChangeSetName=change_set_name, + TemplateBody=t2, + ChangeSetType="UPDATE", + Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p2.items()], + ) + snapshot.match("create-change-set-2", change_set_details) + stack_id = change_set_details["StackId"] + change_set_id = change_set_details["Id"] + aws_client_no_retry.cloudformation.get_waiter("change_set_create_complete").wait( + ChangeSetName=change_set_id + ) + + describe_change_set_with_prop_values = ( + aws_client_no_retry.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=True + ) + ) + snapshot.match("describe-change-set-2-prop-values", describe_change_set_with_prop_values) + describe_change_set_without_prop_values = ( + aws_client_no_retry.cloudformation.describe_change_set( + ChangeSetName=change_set_id, IncludePropertyValues=False + ) + ) + snapshot.match("describe-change-set-2", describe_change_set_without_prop_values) + + execute_results = aws_client_no_retry.cloudformation.execute_change_set( + ChangeSetName=change_set_id + ) + snapshot.match("execute-change-set-2", execute_results) + aws_client_no_retry.cloudformation.get_waiter("stack_update_complete").wait( + StackName=stack_id + ) + + describe = aws_client_no_retry.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][ + 0 + ] + snapshot.match("post-create-2-describe", describe) + + events = capture_per_resource_events(stack_name) + snapshot.match("per-resource-events", events) + + # delete stack + aws_client_no_retry.cloudformation.delete_stack(StackName=stack_id) + aws_client_no_retry.cloudformation.get_waiter("stack_delete_complete").wait( + StackName=stack_id + ) + describe = aws_client_no_retry.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][ + 0 + ] + snapshot.match("delete-describe", describe) + + yield inner diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index 86661fda10bd9..3ccf088f6bbe5 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -1,15 +1,13 @@ import copy import json import os.path -from collections import defaultdict -from typing import Callable import pytest from botocore.exceptions import ClientError +from localstack_snapshot.snapshots.transformer import RegexTransformer -from localstack import config -from localstack.aws.api.cloudformation import StackEvent from localstack.aws.connect import ServiceLevelClientFactory +from localstack.services.cloudformation.v2.utils import is_v2_engine from localstack.testing.aws.cloudformation_utils import ( load_template_file, load_template_raw, @@ -17,7 +15,6 @@ ) from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers -from localstack.utils.functions import call_safe from localstack.utils.strings import short_uid from localstack.utils.sync import ShortCircuitWaitException, poll_condition, wait_until from tests.aws.services.cloudformation.api.test_stacks import ( @@ -25,10 +22,6 @@ ) -def is_v2_engine() -> bool: - return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2" - - class TestUpdates: @markers.aws.validated def test_simple_update_single_resource( @@ -66,9 +59,9 @@ def test_simple_update_single_resource( res.destroy() - # @pytest.mark.skipif( - # condition=not is_v2_engine() and not is_aws_cloud(), reason="Not working in v2 yet" - # ) + @pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Not working in v2 yet" + ) @markers.aws.validated def test_simple_update_two_resources( self, aws_client: ServiceLevelClientFactory, deploy_cfn_template @@ -1214,150 +1207,30 @@ def test_describe_change_set_with_similarly_named_stacks(deploy_cfn_template, aw ) -PerResourceStackEvents = dict[str, list[StackEvent]] - - @pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) class TestCaptureUpdateProcess: - @pytest.fixture - def capture_per_resource_events( - self, - aws_client: ServiceLevelClientFactory, - ) -> Callable[[str], PerResourceStackEvents]: - def capture(stack_name: str) -> PerResourceStackEvents: - events = aws_client.cloudformation.describe_stack_events(StackName=stack_name)[ - "StackEvents" - ] - per_resource_events = defaultdict(list) - for event in events: - if logical_resource_id := event.get("LogicalResourceId"): - per_resource_events[logical_resource_id].append(event) - return per_resource_events - - return capture - - @pytest.fixture - def capture_update_process(self, aws_client, snapshot, cleanups, capture_per_resource_events): - """ - Fixture to deploy a new stack (via creating and executing a change set), then updating the - stack with a second template (via creating and executing a change set). - """ - - stack_name = f"stack-{short_uid()}" - change_set_name = f"cs-{short_uid()}" - - def inner(t1: dict | str, t2: dict | str, p1: dict | None = None, p2: dict | None = None): - if isinstance(t1, dict): - t1 = json.dumps(t1) - elif isinstance(t1, str): - with open(t1) as infile: - t1 = infile.read() - if isinstance(t2, dict): - t2 = json.dumps(t2) - elif isinstance(t2, str): - with open(t2) as infile: - t2 = infile.read() - - p1 = p1 or {} - p2 = p2 or {} - - # deploy original stack - change_set_details = aws_client.cloudformation.create_change_set( - StackName=stack_name, - ChangeSetName=change_set_name, - TemplateBody=t1, - ChangeSetType="CREATE", - Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p1.items()], - ) - snapshot.match("create-change-set-1", change_set_details) - stack_id = change_set_details["StackId"] - change_set_id = change_set_details["Id"] - aws_client.cloudformation.get_waiter("change_set_create_complete").wait( - ChangeSetName=change_set_id - ) - cleanups.append( - lambda: call_safe( - aws_client.cloudformation.delete_change_set, - kwargs=dict(ChangeSetName=change_set_id), - ) - ) - - describe_change_set_with_prop_values = aws_client.cloudformation.describe_change_set( - ChangeSetName=change_set_id, IncludePropertyValues=True - ) - snapshot.match( - "describe-change-set-1-prop-values", describe_change_set_with_prop_values - ) - describe_change_set_without_prop_values = aws_client.cloudformation.describe_change_set( - ChangeSetName=change_set_id, IncludePropertyValues=False - ) - snapshot.match("describe-change-set-1", describe_change_set_without_prop_values) - - execute_results = aws_client.cloudformation.execute_change_set( - ChangeSetName=change_set_id - ) - snapshot.match("execute-change-set-1", execute_results) - aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_id) - - # ensure stack deletion - cleanups.append( - lambda: call_safe( - aws_client.cloudformation.delete_stack, kwargs=dict(StackName=stack_id) - ) - ) - - describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] - snapshot.match("post-create-1-describe", describe) - - # update stack - change_set_details = aws_client.cloudformation.create_change_set( - StackName=stack_name, - ChangeSetName=change_set_name, - TemplateBody=t2, - ChangeSetType="UPDATE", - Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p2.items()], - ) - snapshot.match("create-change-set-2", change_set_details) - stack_id = change_set_details["StackId"] - change_set_id = change_set_details["Id"] - aws_client.cloudformation.get_waiter("change_set_create_complete").wait( - ChangeSetName=change_set_id - ) - - describe_change_set_with_prop_values = aws_client.cloudformation.describe_change_set( - ChangeSetName=change_set_id, IncludePropertyValues=True - ) - snapshot.match( - "describe-change-set-2-prop-values", describe_change_set_with_prop_values - ) - describe_change_set_without_prop_values = aws_client.cloudformation.describe_change_set( - ChangeSetName=change_set_id, IncludePropertyValues=False - ) - snapshot.match("describe-change-set-2", describe_change_set_without_prop_values) - - execute_results = aws_client.cloudformation.execute_change_set( - ChangeSetName=change_set_id - ) - snapshot.match("execute-change-set-2", execute_results) - aws_client.cloudformation.get_waiter("stack_update_complete").wait(StackName=stack_id) - - describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] - snapshot.match("post-create-2-describe", describe) - - events = capture_per_resource_events(stack_name) - snapshot.match("per-resource-events", events) - - # delete stack - aws_client.cloudformation.delete_stack(StackName=stack_id) - aws_client.cloudformation.get_waiter("stack_delete_complete").wait(StackName=stack_id) - describe = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0] - snapshot.match("delete-describe", describe) - - yield inner - @markers.aws.validated def test_direct_update( self, + snapshot, capture_update_process, ): """ @@ -1370,6 +1243,8 @@ def test_direct_update( """ name1 = f"topic-1-{short_uid()}" name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) t1 = { "Resources": { "Foo": { @@ -1390,12 +1265,13 @@ def test_direct_update( }, }, } - - capture_update_process(t1, t2) + capture_update_process(snapshot, t1, t2) @markers.aws.validated + @pytest.mark.skip("Deployment fails, as executor is WIP") def test_dynamic_update( self, + snapshot, capture_update_process, ): """ @@ -1412,6 +1288,8 @@ def test_dynamic_update( """ name1 = f"topic-1-{short_uid()}" name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) t1 = { "Resources": { "Foo": { @@ -1450,12 +1328,16 @@ def test_dynamic_update( }, }, } - - capture_update_process(t1, t2) + capture_update_process(snapshot, t1, t2) @markers.aws.validated + @pytest.mark.skip( + "Template deployment appears to fail on v2 due to unresolved resource dependencies; " + "this should be addressed in the development of the v2 engine executor." + ) def test_parameter_changes( self, + snapshot, capture_update_process, ): """ @@ -1472,6 +1354,8 @@ def test_parameter_changes( """ name1 = f"topic-1-{short_uid()}" name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) t1 = { "Parameters": { "TopicName": { @@ -1496,12 +1380,13 @@ def test_parameter_changes( }, }, } - - capture_update_process(t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) + capture_update_process(snapshot, t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) @markers.aws.validated + @pytest.mark.skip("Deployment fails, as executor is WIP") def test_mappings_with_static_fields( self, + snapshot, capture_update_process, ): """ @@ -1515,15 +1400,14 @@ def test_mappings_with_static_fields( - The CloudFormation engine does not resolve intrinsic function calls when determining the nature of the update """ - name1 = "key1" - name2 = "key2" + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) t1 = { "Mappings": { "MyMap": { - "MyKey": { - name1: "MyTopicName", - name2: "MyNewTopicName", - }, + "MyKey": {"key1": name1, "key2": name2}, }, }, "Resources": { @@ -1534,7 +1418,7 @@ def test_mappings_with_static_fields( "Fn::FindInMap": [ "MyMap", "MyKey", - name1, + "key1", ], }, }, @@ -1554,8 +1438,8 @@ def test_mappings_with_static_fields( "Mappings": { "MyMap": { "MyKey": { - name1: f"MyTopicName{short_uid()}", - name2: f"MyNewTopicName{short_uid()}", + "key1": name1, + "key2": name2, }, }, }, @@ -1567,7 +1451,7 @@ def test_mappings_with_static_fields( "Fn::FindInMap": [ "MyMap", "MyKey", - name2, + "key2", ], }, }, @@ -1583,12 +1467,16 @@ def test_mappings_with_static_fields( }, }, } - - capture_update_process(t1, t2) + capture_update_process(snapshot, t1, t2) @markers.aws.validated + @pytest.mark.skip( + "Template deployment appears to fail on v2 due to unresolved resource dependencies; " + "this should be addressed in the development of the v2 engine executor." + ) def test_mappings_with_parameter_lookup( self, + snapshot, capture_update_process, ): """ @@ -1600,8 +1488,10 @@ def test_mappings_with_parameter_lookup( Conclusions: - The same conclusions as `test_mappings_with_static_fields` """ - name1 = "key1" - name2 = "key2" + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) t1 = { "Parameters": { "TopicName": { @@ -1610,10 +1500,7 @@ def test_mappings_with_parameter_lookup( }, "Mappings": { "MyMap": { - "MyKey": { - name1: f"topic-1-{short_uid()}", - name2: f"topic-2-{short_uid()}", - }, + "MyKey": {"key1": name1, "key2": name2}, }, }, "Resources": { @@ -1642,12 +1529,16 @@ def test_mappings_with_parameter_lookup( }, }, } - - capture_update_process(t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) + capture_update_process(snapshot, t1, t1, p1={"TopicName": "key1"}, p2={"TopicName": "key2"}) @markers.aws.validated + @pytest.mark.skip( + "Template deployment appears to fail on v2 due to unresolved resource dependencies; " + "this should be addressed in the development of the v2 engine executor." + ) def test_conditions( self, + snapshot, capture_update_process, ): """ @@ -1686,12 +1577,18 @@ def test_conditions( } capture_update_process( - t1, t1, p1={"EnvironmentType": "not-prod"}, p2={"EnvironmentType": "prod"} + snapshot, t1, t1, p1={"EnvironmentType": "not-prod"}, p2={"EnvironmentType": "prod"} ) @markers.aws.validated + @pytest.mark.skip( + "Unlike AWS CFN, the update graph understands the dependent resource does not " + "need modification also when the IncludePropertyValues flag is off." + # TODO: we may achieve the same limitation by pruning the resolution of traversals. + ) def test_unrelated_changes_update_propagation( self, + snapshot, capture_update_process, ): """ @@ -1702,6 +1599,7 @@ def test_unrelated_changes_update_propagation( - No update to resource B """ topic_name = f"MyTopic{short_uid()}" + snapshot.add_transformer(RegexTransformer(topic_name, "topic-name")) t1 = { "Resources": { "Parameter1": { @@ -1721,7 +1619,6 @@ def test_unrelated_changes_update_propagation( }, }, } - t2 = { "Resources": { "Parameter1": { @@ -1741,11 +1638,15 @@ def test_unrelated_changes_update_propagation( }, }, } - capture_update_process(t1, t2) + capture_update_process(snapshot, t1, t2) @markers.aws.validated + @pytest.mark.skip( + "Deployment fails however this appears to be unrelated from the update graph building and describe" + ) def test_unrelated_changes_requires_replacement( self, + snapshot, capture_update_process, ): """ @@ -1757,7 +1658,8 @@ def test_unrelated_changes_requires_replacement( """ parameter_name_1 = f"MyParameter{short_uid()}" parameter_name_2 = f"MyParameter{short_uid()}" - + snapshot.add_transformer(RegexTransformer(parameter_name_1, "parameter-1-name")) + snapshot.add_transformer(RegexTransformer(parameter_name_2, "parameter-2-name")) t1 = { "Resources": { "Parameter1": { @@ -1796,5 +1698,178 @@ def test_unrelated_changes_requires_replacement( }, }, } + capture_update_process(snapshot, t1, t2) - capture_update_process(t1, t2) + @markers.aws.validated + @pytest.mark.skip("Executor is WIP") + @pytest.mark.parametrize( + "template", + [ + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + } + }, + }, + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "param-name", + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, + }, + }, + }, + }, + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, + }, + }, + }, + }, + { + "Parameters": { + "ParameterValue": { + "Type": "String", + "Default": "value-1", + "AllowedValues": ["value-1", "value-2"], + } + }, + "Conditions": { + "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"]} + }, + "Resources": { + "SSMParameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + "SSMParameter2": { + "Type": "AWS::SSM::Parameter", + "Condition": "ShouldCreateParameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + }, + }, + ], + ids=[ + "change_dynamic", + "change_unrelated_property", + "change_unrelated_property_not_create_only", + "change_parameter_for_condition_create_resource", + ], + ) + def test_base_dynamic_parameter_scenarios( + self, + snapshot, + capture_update_process, + template, + ): + capture_update_process( + snapshot, + template, + template, + {"ParameterValue": "value-1"}, + {"ParameterValue": "value-2"}, + ) + + @markers.aws.validated + @pytest.mark.skip("Executor is WIP") + @pytest.mark.parametrize( + "template_1, template_2", + [ + ( + { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + "EnvironmentA", + "ParameterValue", + ] + }, + }, + } + }, + }, + { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-2"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + "EnvironmentA", + "ParameterValue", + ] + }, + }, + } + }, + }, + ) + ], + ids=["update_string_referencing_resource"], + ) + def test_base_mapping_scenarios( + self, + snapshot, + capture_update_process, + template_1, + template_2, + ): + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json index ae7ab8f10c674..0020e238e7865 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json @@ -500,11 +500,11 @@ } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_direct_update": { - "recorded-date": "27-03-2025, 15:27:22", + "recorded-date": "01-04-2025, 08:32:30", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -512,15 +512,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", - "ChangeSetName": "cs-4eddd7ee", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "topic-1-699c8a3c" + "TopicName": "topic-1" } }, "Details": [], @@ -537,8 +537,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -547,8 +547,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", - "ChangeSetName": "cs-4eddd7ee", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -566,8 +566,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -581,7 +581,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/4c429328-3674-4123-bfb2-b5ae2b3eacb2", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -591,14 +591,14 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -606,20 +606,20 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", - "ChangeSetName": "cs-4eddd7ee", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "TopicName": "topic-2-c8bc3e89" + "TopicName": "topic-2" } }, "BeforeContext": { "Properties": { - "TopicName": "topic-1-699c8a3c" + "TopicName": "topic-1" } }, "Details": [ @@ -627,10 +627,10 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "topic-2-c8bc3e89", + "AfterValue": "topic-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-699c8a3c", + "BeforeValue": "topic-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -638,7 +638,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -654,8 +654,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -664,8 +664,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", - "ChangeSetName": "cs-4eddd7ee", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -682,7 +682,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -698,8 +698,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -713,7 +713,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-4eddd7ee/86c41461-f2e5-4a6a-9224-72c94eed3fc6", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -723,99 +723,99 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Foo": [ { - "EventId": "Foo-a48cca6f-40de-4a25-99f5-f9b9ec67da45", + "EventId": "Foo-ce4449a5-3be2-4c7d-9737-e408c3ca7772", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Foo-74d62981-96eb-4499-97bd-1de15cca0f73", + "EventId": "Foo-c0f9138e-b5fd-45c2-976e-f1fafee907d7", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-c8bc3e89", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-c8bc3e89" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-c8bc3e89", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-c8bc3e89" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-2-c8bc3e89" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-699c8a3c" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-699c8a3c", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-699c8a3c" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -823,77 +823,77 @@ "LogicalResourceId": "Foo", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "topic-1-699c8a3c" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-1ec85b6e": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-1ec85b6e", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -908,19 +908,19 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-1ec85b6e/b5debad0-0b1f-11f0-8cee-0a1aeaf94321", - "StackName": "stack-1ec85b6e", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { - "recorded-date": "27-03-2025, 15:29:21", + "recorded-date": "01-04-2025, 12:30:53", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -928,15 +928,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", - "ChangeSetName": "cs-7ca0678c", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "topic-1-efcd7dc5" + "TopicName": "topic-1" } }, "Details": [], @@ -970,8 +970,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -980,8 +980,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", - "ChangeSetName": "cs-7ca0678c", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -1009,8 +1009,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1024,7 +1024,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/e67e5e7b-d2a1-4cf6-b046-229f28f44644", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -1034,14 +1034,14 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1049,20 +1049,20 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", - "ChangeSetName": "cs-7ca0678c", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "TopicName": "topic-2-82db7bdb" + "TopicName": "topic-2" } }, "BeforeContext": { "Properties": { - "TopicName": "topic-1-efcd7dc5" + "TopicName": "topic-1" } }, "Details": [ @@ -1070,10 +1070,10 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "topic-2-82db7bdb", + "AfterValue": "topic-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-efcd7dc5", + "BeforeValue": "topic-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -1081,7 +1081,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -1102,33 +1102,33 @@ }, "BeforeContext": { "Properties": { - "Value": "topic-1-efcd7dc5", + "Value": "topic-1", "Type": "String" } }, "Details": [ { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-efcd7dc5", + "BeforeValue": "topic-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" } }, { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-efcd7dc5", + "BeforeValue": "topic-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" @@ -1136,7 +1136,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -1151,8 +1151,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1161,8 +1161,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", - "ChangeSetName": "cs-7ca0678c", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -1179,7 +1179,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -1205,7 +1205,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -1220,8 +1220,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1235,7 +1235,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-7ca0678c/ede49433-decf-4d85-b3d0-33a278d36905", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -1245,99 +1245,99 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Foo": [ { - "EventId": "Foo-d526ef1f-2313-46f1-822d-917598c7fb75", + "EventId": "Foo-df917f95-bc5f-461d-9a94-80d49271b9c0", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Foo-9b9f85bf-2ddb-4098-b926-dc3b12359989", + "EventId": "Foo-cccf775d-9ee4-4b65-afd3-68bd00248a16", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-82db7bdb", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-82db7bdb" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-82db7bdb", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-82db7bdb" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-2-82db7bdb" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-efcd7dc5" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-efcd7dc5", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-efcd7dc5" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -1345,12 +1345,12 @@ "LogicalResourceId": "Foo", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "topic-1-efcd7dc5" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -1358,58 +1358,58 @@ { "EventId": "Parameter-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "ResourceProperties": { "Type": "String", - "Value": "topic-2-82db7bdb" + "Value": "topic-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "ResourceProperties": { "Type": "String", - "Value": "topic-2-82db7bdb" + "Value": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "ResourceProperties": { "Type": "String", - "Value": "topic-1-efcd7dc5" + "Value": "topic-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-gE44Xp97pzMR", + "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", "ResourceProperties": { "Type": "String", - "Value": "topic-1-efcd7dc5" + "Value": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -1418,77 +1418,77 @@ "PhysicalResourceId": "", "ResourceProperties": { "Type": "String", - "Value": "topic-1-efcd7dc5" + "Value": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-097cd147": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-097cd147", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -1503,19 +1503,19 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-097cd147/faa20820-0b1f-11f0-982d-02c48abc604d", - "StackName": "stack-097cd147", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_parameter_changes": { - "recorded-date": "27-03-2025, 15:31:22", + "recorded-date": "01-04-2025, 12:43:36", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1523,15 +1523,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", - "ChangeSetName": "cs-5be8adbc", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "topic-1-fa993773" + "TopicName": "topic-1" } }, "Details": [], @@ -1567,12 +1567,12 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-1-fa993773" + "ParameterValue": "topic-1" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1581,8 +1581,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", - "ChangeSetName": "cs-5be8adbc", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -1612,12 +1612,12 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-1-fa993773" + "ParameterValue": "topic-1" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1631,7 +1631,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/cdf2ac63-ac55-45e4-99e4-2d2fff3f2c5f", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -1643,18 +1643,18 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-1-fa993773" + "ParameterValue": "topic-1" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1662,20 +1662,20 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", - "ChangeSetName": "cs-5be8adbc", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "TopicName": "topic-2-c7672df9" + "TopicName": "topic-2" } }, "BeforeContext": { "Properties": { - "TopicName": "topic-1-fa993773" + "TopicName": "topic-1" } }, "Details": [ @@ -1684,10 +1684,10 @@ "ChangeSource": "ParameterReference", "Evaluation": "Static", "Target": { - "AfterValue": "topic-2-c7672df9", + "AfterValue": "topic-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-fa993773", + "BeforeValue": "topic-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -1697,10 +1697,10 @@ "ChangeSource": "DirectModification", "Evaluation": "Dynamic", "Target": { - "AfterValue": "topic-2-c7672df9", + "AfterValue": "topic-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-fa993773", + "BeforeValue": "topic-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -1708,7 +1708,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -1729,33 +1729,33 @@ }, "BeforeContext": { "Properties": { - "Value": "topic-1-fa993773", + "Value": "topic-1", "Type": "String" } }, "Details": [ { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-fa993773", + "BeforeValue": "topic-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" } }, { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-fa993773", + "BeforeValue": "topic-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" @@ -1763,7 +1763,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -1780,12 +1780,12 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-2-c7672df9" + "ParameterValue": "topic-2" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1794,8 +1794,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", - "ChangeSetName": "cs-5be8adbc", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -1822,7 +1822,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -1848,7 +1848,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -1865,12 +1865,12 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-2-c7672df9" + "ParameterValue": "topic-2" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1884,7 +1884,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-5be8adbc/f67927f3-7514-48a8-97c2-ae7ccad2b3b0", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -1896,103 +1896,103 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-2-c7672df9" + "ParameterValue": "topic-2" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Foo": [ { - "EventId": "Foo-d28e4326-c3ef-4d26-aab1-592325e91de8", + "EventId": "Foo-56f4760f-0517-4079-926e-6b9a0d48622e", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Foo-e94fb6b6-124f-4998-b93b-67032802c460", + "EventId": "Foo-9f0f56a9-9622-419d-bd2a-83e387b32c4b", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-c7672df9", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-c7672df9" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-c7672df9", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", "ResourceProperties": { - "TopicName": "topic-2-c7672df9" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-2-c7672df9" + "TopicName": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-fa993773" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-fa993773", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", "ResourceProperties": { - "TopicName": "topic-1-fa993773" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -2000,12 +2000,12 @@ "LogicalResourceId": "Foo", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "topic-1-fa993773" + "TopicName": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -2013,58 +2013,58 @@ { "EventId": "Parameter-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "ResourceProperties": { "Type": "String", - "Value": "topic-2-c7672df9" + "Value": "topic-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "ResourceProperties": { "Type": "String", - "Value": "topic-2-c7672df9" + "Value": "topic-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "ResourceProperties": { "Type": "String", - "Value": "topic-1-fa993773" + "Value": "topic-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-QEBuncjAsBXv", + "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", "ResourceProperties": { "Type": "String", - "Value": "topic-1-fa993773" + "Value": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -2073,77 +2073,77 @@ "PhysicalResourceId": "", "ResourceProperties": { "Type": "String", - "Value": "topic-1-fa993773" + "Value": "topic-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-7e33c12d": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-7e33c12d", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -2160,23 +2160,23 @@ "Parameters": [ { "ParameterKey": "TopicName", - "ParameterValue": "topic-2-c7672df9" + "ParameterValue": "topic-2" } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-7e33c12d/41d989c0-0b20-11f0-a278-065c9a7b14c7", - "StackName": "stack-7e33c12d", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { - "recorded-date": "27-03-2025, 15:33:23", + "recorded-date": "01-04-2025, 13:20:51", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -2184,15 +2184,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", - "ChangeSetName": "cs-98b12539", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "MyTopicName" + "TopicName": "topic-name-1" } }, "Details": [], @@ -2226,8 +2226,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2236,8 +2236,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", - "ChangeSetName": "cs-98b12539", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2265,8 +2265,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2280,7 +2280,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/fb2fbe94-871a-48d5-b443-bd123e7f6d86", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -2290,14 +2290,14 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -2305,20 +2305,20 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", - "ChangeSetName": "cs-98b12539", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "TopicName": "MyNewTopicName981a23e2" + "TopicName": "topic-name-2" } }, "BeforeContext": { "Properties": { - "TopicName": "MyTopicName" + "TopicName": "topic-name-1" } }, "Details": [ @@ -2326,10 +2326,10 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "MyNewTopicName981a23e2", + "AfterValue": "topic-name-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "MyTopicName", + "BeforeValue": "topic-name-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -2337,7 +2337,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -2358,33 +2358,33 @@ }, "BeforeContext": { "Properties": { - "Value": "MyTopicName", + "Value": "topic-name-1", "Type": "String" } }, "Details": [ { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "MyTopicName", + "BeforeValue": "topic-name-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" } }, { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", "Target": { "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "MyTopicName", + "BeforeValue": "topic-name-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" @@ -2392,7 +2392,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -2407,8 +2407,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2417,8 +2417,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", - "ChangeSetName": "cs-98b12539", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2435,7 +2435,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -2461,7 +2461,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -2476,8 +2476,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2491,7 +2491,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-98b12539/f7f801d8-dcc1-4d62-a887-19137af67d72", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -2501,99 +2501,99 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Foo": [ { - "EventId": "Foo-b04ac465-abfd-462a-99ee-64eee54e03bf", + "EventId": "Foo-d832b7fd-78cf-4fba-a6fa-e440862a0428", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Foo-641ad9a6-b755-4525-a678-b97a5df4f528", + "EventId": "Foo-19e3d928-52a1-4c4f-98ef-044bf9bb68e4", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyNewTopicName981a23e2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", "ResourceProperties": { - "TopicName": "MyNewTopicName981a23e2" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyNewTopicName981a23e2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", "ResourceProperties": { - "TopicName": "MyNewTopicName981a23e2" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "MyNewTopicName981a23e2" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "MyTopicName" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:MyTopicName", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "MyTopicName" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -2601,12 +2601,12 @@ "LogicalResourceId": "Foo", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "MyTopicName" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -2614,58 +2614,58 @@ { "EventId": "Parameter-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "ResourceProperties": { "Type": "String", - "Value": "MyNewTopicName981a23e2" + "Value": "topic-name-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "ResourceProperties": { "Type": "String", - "Value": "MyNewTopicName981a23e2" + "Value": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "ResourceProperties": { "Type": "String", - "Value": "MyTopicName" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-Rq3K1HiaCxUY", + "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", "ResourceProperties": { "Type": "String", - "Value": "MyTopicName" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -2674,77 +2674,77 @@ "PhysicalResourceId": "", "ResourceProperties": { "Type": "String", - "Value": "MyTopicName" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-32de1443": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-32de1443", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -2759,19 +2759,19 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-32de1443/89e26520-0b20-11f0-a45a-06491e11d8cf", - "StackName": "stack-32de1443", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_conditions": { - "recorded-date": "27-03-2025, 15:34:24", + "recorded-date": "01-04-2025, 14:34:35", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -2779,8 +2779,8 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", - "ChangeSetName": "cs-45c519bb", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2808,8 +2808,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2818,8 +2818,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", - "ChangeSetName": "cs-45c519bb", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2843,8 +2843,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2858,7 +2858,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/9c2a9f3f-2865-4f2c-b129-ac6c0bbd920d", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -2874,14 +2874,14 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -2889,8 +2889,8 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", - "ChangeSetName": "cs-45c519bb", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2921,8 +2921,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2931,8 +2931,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", - "ChangeSetName": "cs-45c519bb", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -2956,8 +2956,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -2971,7 +2971,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-45c519bb/99a01d00-ad64-43b1-a79f-4797f5006a24", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -2987,8 +2987,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, @@ -2997,24 +2997,24 @@ { "EventId": "Bucket-CREATE_COMPLETE-date", "LogicalResourceId": "Bucket", - "PhysicalResourceId": "stack-60c7d006-bucket-f9mgwephfnj6", + "PhysicalResourceId": "-bucket-fkzovuwylzkf", "ResourceProperties": {}, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Bucket-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Bucket", - "PhysicalResourceId": "stack-60c7d006-bucket-f9mgwephfnj6", + "PhysicalResourceId": "-bucket-fkzovuwylzkf", "ResourceProperties": {}, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -3024,8 +3024,8 @@ "ResourceProperties": {}, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -3033,21 +3033,21 @@ { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-BrUaQwZmp5R5", + "PhysicalResourceId": "CFN-Parameter-1xnvY3TGhTRc", "ResourceProperties": { "Type": "String", "Value": "test" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-BrUaQwZmp5R5", + "PhysicalResourceId": "CFN-Parameter-1xnvY3TGhTRc", "ResourceProperties": { "Type": "String", "Value": "test" @@ -3055,8 +3055,8 @@ "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -3069,73 +3069,73 @@ }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-60c7d006": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-60c7d006", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -3156,19 +3156,19 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-60c7d006/e08d9700-0b20-11f0-8a9b-06c1fa4c13b9", - "StackName": "stack-60c7d006", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { - "recorded-date": "27-03-2025, 15:34:51", + "recorded-date": "01-04-2025, 16:40:03", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3176,15 +3176,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", - "ChangeSetName": "cs-67acddb8", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "Value": "MyTopiceb1687cb", + "Value": "topic-name", "Type": "String", "Description": "original" } @@ -3220,8 +3220,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3230,8 +3230,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", - "ChangeSetName": "cs-67acddb8", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3259,8 +3259,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3274,7 +3274,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/e4e79f37-01a2-48cd-9c7a-af86fab8da07", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -3284,14 +3284,14 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3299,22 +3299,22 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", - "ChangeSetName": "cs-67acddb8", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "Value": "MyTopiceb1687cb", + "Value": "topic-name", "Type": "String", "Description": "changed" } }, "BeforeContext": { "Properties": { - "Value": "MyTopiceb1687cb", + "Value": "topic-name", "Type": "String", "Description": "original" } @@ -3335,7 +3335,7 @@ } ], "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -3350,8 +3350,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3360,8 +3360,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", - "ChangeSetName": "cs-67acddb8", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3378,7 +3378,7 @@ } ], "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -3403,7 +3403,7 @@ } ], "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -3418,8 +3418,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3433,7 +3433,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67acddb8/6838f139-de8d-4d8c-bb81-295856e4fd8a", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -3443,8 +3443,8 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, @@ -3453,62 +3453,62 @@ { "EventId": "Parameter1-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "ResourceProperties": { "Type": "String", "Description": "changed", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "ResourceProperties": { "Type": "String", "Description": "changed", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "ResourceProperties": { "Type": "String", "Description": "original", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-zYUBi9de23gM", + "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", "ResourceProperties": { "Type": "String", "Description": "original", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -3518,12 +3518,12 @@ "ResourceProperties": { "Type": "String", "Description": "original", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -3531,30 +3531,30 @@ { "EventId": "Parameter2-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", "ResourceProperties": { "Type": "String", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter2-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-g4MkhGwgQYEW", + "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", "ResourceProperties": { "Type": "String", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -3563,77 +3563,77 @@ "PhysicalResourceId": "", "ResourceProperties": { "Type": "String", - "Value": "MyTopiceb1687cb" + "Value": "topic-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-bdbd0fde": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-bdbd0fde", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -3648,19 +3648,19 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-bdbd0fde/f680a390-0b20-11f0-a082-0a359e011d35", - "StackName": "stack-bdbd0fde", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_requires_replacement": { - "recorded-date": "27-03-2025, 15:35:20", + "recorded-date": "01-04-2025, 16:46:22", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3668,8 +3668,8 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", - "ChangeSetName": "cs-3cecf361", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3678,7 +3678,7 @@ "Properties": { "Value": "value", "Type": "String", - "Name": "MyParameter9469f4fe" + "Name": "parameter-1-name" } }, "Details": [], @@ -3712,8 +3712,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3722,8 +3722,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", - "ChangeSetName": "cs-3cecf361", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3751,8 +3751,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3766,7 +3766,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/3562d4a1-50e7-458c-b877-4f7b41fa383d", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -3776,14 +3776,14 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3791,8 +3791,8 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", - "ChangeSetName": "cs-3cecf361", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3801,14 +3801,14 @@ "Properties": { "Value": "value", "Type": "String", - "Name": "MyParameter989f40e8" + "Name": "parameter-2-name" } }, "BeforeContext": { "Properties": { "Value": "value", "Type": "String", - "Name": "MyParameter9469f4fe" + "Name": "parameter-1-name" } }, "Details": [ @@ -3816,10 +3816,10 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "MyParameter989f40e8", + "AfterValue": "parameter-2-name", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "MyParameter9469f4fe", + "BeforeValue": "parameter-1-name", "Name": "Name", "Path": "/Properties/Name", "RequiresRecreation": "Always" @@ -3827,7 +3827,7 @@ } ], "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SSM::Parameter", @@ -3882,7 +3882,7 @@ } ], "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -3897,8 +3897,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3907,8 +3907,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", - "ChangeSetName": "cs-3cecf361", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -3925,7 +3925,7 @@ } ], "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SSM::Parameter", @@ -3951,7 +3951,7 @@ } ], "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -3966,8 +3966,8 @@ "IncludeNestedStacks": false, "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -3981,7 +3981,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-3cecf361/4b2e28ef-60cc-4ce9-baae-c32723f7f702", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -3991,109 +3991,109 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Parameter1": [ { - "EventId": "Parameter1-b6fe9c4b-ee5a-4ff1-bd74-a97f2ac48892", + "EventId": "Parameter1-ff8b6634-0c41-421e-93cc-6cc7d8dae415", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Parameter1-bfc08cb6-1a40-471f-8598-88f0bd62289a", + "EventId": "Parameter1-399217af-3b0b-4d34-8220-daee4c9a3f59", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter989f40e8", + "PhysicalResourceId": "parameter-2-name", "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter989f40e8" + "Name": "parameter-2-name" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter989f40e8", + "PhysicalResourceId": "parameter-2-name", "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter989f40e8" + "Name": "parameter-2-name" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter989f40e8" + "Name": "parameter-2-name" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter9469f4fe" + "Name": "parameter-1-name" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter1-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "MyParameter9469f4fe", + "PhysicalResourceId": "parameter-1-name", "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter9469f4fe" + "Name": "parameter-1-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -4103,12 +4103,12 @@ "ResourceProperties": { "Type": "String", "Value": "value", - "Name": "MyParameter9469f4fe" + "Name": "parameter-1-name" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -4116,21 +4116,21 @@ { "EventId": "Parameter2-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", "ResourceProperties": { "Type": "String", "Value": "value" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter2-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-HcKZbIDYysEd", + "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", "ResourceProperties": { "Type": "String", "Value": "value" @@ -4138,8 +4138,8 @@ "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -4152,73 +4152,73 @@ }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-859f670a": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-859f670a", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -4233,19 +4233,19 @@ "LastUpdatedTime": "datetime", "NotificationARNs": [], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-859f670a/06639060-0b21-11f0-a6bf-0a5c67472731", - "StackName": "stack-859f670a", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } } }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { - "recorded-date": "27-03-2025, 15:43:03", + "recorded-date": "01-04-2025, 13:31:33", "recorded-content": { "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -4253,15 +4253,15 @@ }, "describe-change-set-1-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", - "ChangeSetName": "cs-67879e0f", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "topic-1-867f6b77" + "TopicName": "topic-name-1" } }, "Details": [], @@ -4301,8 +4301,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4311,8 +4311,8 @@ }, "describe-change-set-1": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", - "ChangeSetName": "cs-67879e0f", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -4346,8 +4346,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4361,7 +4361,7 @@ } }, "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/53cfe51c-a3a7-471e-8016-2f15bfba38d5", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -4377,14 +4377,14 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "CREATE_COMPLETE", "Tags": [] }, "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -4392,45 +4392,45 @@ }, "describe-change-set-2-prop-values": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", - "ChangeSetName": "cs-67879e0f", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { "Action": "Modify", "AfterContext": { "Properties": { - "TopicName": "topic-2-8e2a426b" + "TopicName": "topic-name-2" } }, "BeforeContext": { "Properties": { - "TopicName": "topic-1-867f6b77" + "TopicName": "topic-name-1" } }, "Details": [ { - "CausingEntity": "TopicName", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", "Target": { - "AfterValue": "topic-2-8e2a426b", + "AfterValue": "topic-name-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-867f6b77", + "BeforeValue": "topic-name-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" } }, { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", "Target": { - "AfterValue": "topic-2-8e2a426b", + "AfterValue": "topic-name-2", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-867f6b77", + "BeforeValue": "topic-name-1", "Name": "TopicName", "Path": "/Properties/TopicName", "RequiresRecreation": "Always" @@ -4438,7 +4438,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -4459,7 +4459,7 @@ }, "BeforeContext": { "Properties": { - "Value": "topic-1-867f6b77", + "Value": "topic-name-1", "Type": "String" } }, @@ -4472,7 +4472,7 @@ "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-867f6b77", + "BeforeValue": "topic-name-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" @@ -4485,7 +4485,7 @@ "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "topic-1-867f6b77", + "BeforeValue": "topic-name-1", "Name": "Value", "Path": "/Properties/Value", "RequiresRecreation": "Never" @@ -4493,7 +4493,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -4514,8 +4514,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4524,8 +4524,8 @@ }, "describe-change-set-2": { "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", - "ChangeSetName": "cs-67879e0f", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", "Changes": [ { "ResourceChange": { @@ -4552,7 +4552,7 @@ } ], "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "PolicyAction": "ReplaceAndDelete", "Replacement": "True", "ResourceType": "AWS::SNS::Topic", @@ -4578,7 +4578,7 @@ } ], "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", "Scope": [ @@ -4599,8 +4599,8 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Status": "CREATE_COMPLETE", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4614,7 +4614,7 @@ } }, "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/cs-67879e0f/1d67a815-9c61-4567-a443-91947ca1da8e", + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, "DriftInformation": { @@ -4630,99 +4630,99 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "UPDATE_COMPLETE", "Tags": [] }, "per-resource-events": { "Foo": [ { - "EventId": "Foo-329b4b0b-ea37-4fc1-9c0a-6fd7405bdaa0", + "EventId": "Foo-d149fa4f-7f1d-41a2-8db0-6bb7f7f78548", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Foo-eaeea20b-89f6-4f3b-a36d-412ef7b36c71", + "EventId": "Foo-34113190-b651-4ec5-84e2-b6324388325d", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-8e2a426b", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", "ResourceProperties": { - "TopicName": "topic-2-8e2a426b" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2-8e2a426b", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", "ResourceProperties": { - "TopicName": "topic-2-8e2a426b" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "topic-2-8e2a426b" + "TopicName": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_COMPLETE-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "topic-1-867f6b77" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Foo-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1-867f6b77", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceProperties": { - "TopicName": "topic-1-867f6b77" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -4730,12 +4730,12 @@ "LogicalResourceId": "Foo", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "topic-1-867f6b77" + "TopicName": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], @@ -4743,58 +4743,58 @@ { "EventId": "Parameter-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "ResourceProperties": { "Type": "String", - "Value": "topic-2-8e2a426b" + "Value": "topic-name-2" }, "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "ResourceProperties": { "Type": "String", - "Value": "topic-2-8e2a426b" + "Value": "topic-name-2" }, "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "ResourceProperties": { "Type": "String", - "Value": "topic-1-867f6b77" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-tqHKonQiUmzm", + "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", "ResourceProperties": { "Type": "String", - "Value": "topic-1-867f6b77" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { @@ -4803,77 +4803,77 @@ "PhysicalResourceId": "", "ResourceProperties": { "Type": "String", - "Value": "topic-1-867f6b77" + "Value": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ], - "stack-ac935456": [ + "": [ { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" }, { "EventId": "", - "LogicalResourceId": "stack-ac935456", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", "ResourceStatus": "REVIEW_IN_PROGRESS", "ResourceStatusReason": "User Initiated", "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "Timestamp": "timestamp" } ] @@ -4894,8 +4894,2369 @@ } ], "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack/stack-ac935456/e343c0e0-0b21-11f0-894f-06490517416d", - "StackName": "stack-ac935456", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_dynamic]": { + "recorded-date": "03-04-2025, 07:11:45", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property]": { + "recorded-date": "03-04-2025, 07:12:11", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String", + "Name": "param-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter1", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter2", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String", + "Name": "param-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String", + "Name": "param-name" + } + }, + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Parameter1.Name", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter1": [ + { + "EventId": "Parameter1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "ResourceProperties": { + "Type": "String", + "Value": "value-2", + "Name": "param-name" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "ResourceProperties": { + "Type": "String", + "Value": "value-2", + "Name": "param-name" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "ResourceProperties": { + "Type": "String", + "Value": "value-1", + "Name": "param-name" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "param-name", + "ResourceProperties": { + "Type": "String", + "Value": "value-1", + "Name": "param-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1", + "Name": "param-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter2": [ + { + "EventId": "Parameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", + "ResourceProperties": { + "Type": "String", + "Value": "param-name" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", + "ResourceProperties": { + "Type": "String", + "Value": "param-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "param-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property_not_create_only]": { + "recorded-date": "03-04-2025, 07:12:37", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter1", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter2", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Parameter1.Type", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter1": [ + { + "EventId": "Parameter1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter2": [ + { + "EventId": "Parameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", + "ResourceProperties": { + "Type": "String", + "Value": "String" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", + "ResourceProperties": { + "Type": "String", + "Value": "String" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "String" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_parameter_for_condition_create_resource]": { + "recorded-date": "03-04-2025, 07:13:01", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "first", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "SSMParameter1", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SSMParameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "first", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "SSMParameter2", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SSMParameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SSMParameter1": [ + { + "EventId": "SSMParameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "CFN-SSMParameter1-Rk9HUEXJeXov", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "CFN-SSMParameter1-Rk9HUEXJeXov", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "SSMParameter2": [ + { + "EventId": "SSMParameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "CFN-SSMParameter2-esnxaJTjOAZM", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "CFN-SSMParameter2-esnxaJTjOAZM", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_condition_scenarios[condition_update_create_resource]": { + "recorded-date": "03-04-2025, 07:16:52", + "recorded-content": {} + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_mapping_scenarios[update_string_referencing_resource]": { + "recorded-date": "03-04-2025, 07:23:48", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "MySSMParameter", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "MySSMParameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "MySSMParameter": [ + { + "EventId": "MySSMParameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_COMPLETE-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", "StackStatus": "DELETE_COMPLETE", "Tags": [] } diff --git a/tests/aws/services/cloudformation/api/test_changesets.validation.json b/tests/aws/services/cloudformation/api/test_changesets.validation.json index 60cb1602919a3..caa6ed4e295d0 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.validation.json +++ b/tests/aws/services/cloudformation/api/test_changesets.validation.json @@ -1,27 +1,42 @@ { + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_dynamic]": { + "last_validated_date": "2025-04-03T07:11:44+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_parameter_for_condition_create_resource]": { + "last_validated_date": "2025-04-03T07:13:00+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property]": { + "last_validated_date": "2025-04-03T07:12:11+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property_not_create_only]": { + "last_validated_date": "2025-04-03T07:12:37+00:00" + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_mapping_scenarios[update_string_referencing_resource]": { + "last_validated_date": "2025-04-03T07:23:48+00:00" + }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_conditions": { - "last_validated_date": "2025-03-27T15:34:24+00:00" + "last_validated_date": "2025-04-01T14:34:35+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_direct_update": { - "last_validated_date": "2025-03-27T15:27:22+00:00" + "last_validated_date": "2025-04-01T08:32:30+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { - "last_validated_date": "2025-03-27T15:29:21+00:00" + "last_validated_date": "2025-04-01T12:30:53+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { - "last_validated_date": "2025-03-27T15:43:02+00:00" + "last_validated_date": "2025-04-01T13:31:33+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { - "last_validated_date": "2025-03-27T15:33:23+00:00" + "last_validated_date": "2025-04-01T13:20:50+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_parameter_changes": { - "last_validated_date": "2025-03-27T15:31:22+00:00" + "last_validated_date": "2025-04-01T12:43:36+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_requires_replacement": { - "last_validated_date": "2025-03-27T15:35:20+00:00" + "last_validated_date": "2025-04-01T16:46:22+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { - "last_validated_date": "2025-03-27T15:34:51+00:00" + "last_validated_date": "2025-04-01T16:40:03+00:00" }, "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_simple_update_two_resources": { "last_validated_date": "2025-04-02T10:05:26+00:00" diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py new file mode 100644 index 0000000000000..c4c9954eee9c1 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py @@ -0,0 +1,313 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) +class TestChangeSetFnGetAttr: + @markers.aws.validated + def test_resource_addition( + self, + snapshot, + capture_update_process, + ): + # Modify the Value property of a resource to a different literal + # while keeping the dependency via Fn::GetAtt intact. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @pytest.mark.skip(reason="See FIXME in aws_sns_provider::delete") + @markers.aws.validated + def test_resource_deletion( + self, + snapshot, + capture_update_process, + ): + # Modify the Value property of a resource to a different literal + # while keeping the dependency via Fn::GetAtt intact. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: AWS incorrectly does not list the second topic as + # needing modifying, however it needs to + "describe-change-set-2-prop-values..Changes", + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change( + self, + snapshot, + capture_update_process, + ): + # Modify the Value property of a resource to a different literal + # while keeping the dependency via Fn::GetAtt intact. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-2"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: AWS incorrectly does not list the second and third topic as + # needing modifying, however it needs to + "describe-change-set-2-prop-values..Changes", + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change_in_get_attr_chain( + self, + snapshot, + capture_update_process, + ): + # Modify the Value property of a resource to a different literal + # while keeping the dependency via Fn::GetAtt intact. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + name3 = f"topic-name-3-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + snapshot.add_transformer(RegexTransformer(name3, "topic-name-3")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + "Topic3": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name3, + "DisplayName": {"Fn::GetAtt": ["Topic2", "DisplayName"]}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-2"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + "Topic3": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name3, + "DisplayName": {"Fn::GetAtt": ["Topic2", "DisplayName"]}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: AWS appears to incorrectly evaluate the new resource's DisplayName property + # to the old value of the resource being referenced. The describer instead masks + # this value with KNOWN_AFTER_APPLY. The update graph would be able to compute the + # correct new value, however in an effort to match the general behaviour of AWS CFN + # this is being masked as it is updated. + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName", + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change_with_dependent_addition( + self, + snapshot, + capture_update_process, + ): + # Modify the Value property of a resource to a different literal + # while keeping the dependency via Fn::GetAtt intact. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-2"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_immutable_property_update_causes_resource_replacement( + self, + snapshot, + capture_update_process, + ): + # Changing TopicName in Topic1 from represents an immutable property update. + # This should force the resource to be replaced, rather than updated in place. + name1 = f"topic-name-1-{long_uid()}" + name1_update = f"updated-topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name1_update, "updated-topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "value"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1_update, "DisplayName": "new_value"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::GetAtt": ["Topic1", "DisplayName"]}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.snapshot.json new file mode 100644 index 0000000000000..c9a382f83c5d3 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.snapshot.json @@ -0,0 +1,3020 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change": { + "recorded-date": "08-04-2025, 11:24:14", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Topic1.DisplayName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_immutable_property_update_causes_resource_replacement": { + "recorded-date": "08-04-2025, 12:17:00", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "new_value", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "updated-topic-name-1", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "value", + "TopicName": "topic-name-2" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "CausingEntity": "Topic1.DisplayName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Topic1.DisplayName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-7f5fe7ea-9367-43f1-8b98-aa0cef118b00", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-21abbdc1-8335-4dd0-ad4a-f8900e5d49df", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:updated-topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:updated-topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change_with_dependent_addition": { + "recorded-date": "08-04-2025, 12:20:19", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_resource_addition": { + "recorded-date": "08-04-2025, 12:33:53", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_resource_deletion": { + "recorded-date": "08-04-2025, 12:36:41", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-0996d20a-f076-4df0-9fd0-ca5dfcfc0321", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-dfb75ba6-f05f-4970-818e-7e3127cef7d2", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change_in_get_attr_chain": { + "recorded-date": "08-04-2025, 14:46:11", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-3" + } + }, + "Details": [], + "LogicalResourceId": "Topic3", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic3", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Topic1.DisplayName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Topic2.DisplayName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic3": [ + { + "EventId": "Topic3-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.validation.json new file mode 100644 index 0000000000000..b134dc47b4ce5 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.validation.json @@ -0,0 +1,20 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change": { + "last_validated_date": "2025-04-08T11:24:14+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change_in_get_attr_chain": { + "last_validated_date": "2025-04-08T14:46:11+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_direct_attribute_value_change_with_dependent_addition": { + "last_validated_date": "2025-04-08T12:20:18+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_immutable_property_update_causes_resource_replacement": { + "last_validated_date": "2025-04-08T12:17:00+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_resource_addition": { + "last_validated_date": "2025-04-08T12:33:53+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py::TestChangeSetFnGetAttr::test_resource_deletion": { + "last_validated_date": "2025-04-08T12:36:40+00:00" + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.py b/tests/aws/services/cloudformation/v2/test_change_set_ref.py new file mode 100644 index 0000000000000..01f90058aa3c5 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.py @@ -0,0 +1,309 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) +class TestChangeSetRef: + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: preproc is not able to resolve references to deployed resources' physical id + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName" + ] + ) + @markers.aws.validated + def test_resource_addition( + self, + snapshot, + capture_update_process, + ): + # Add a new resource (Topic2) that uses Ref to reference Topic1. + # For SNS topics, Ref typically returns the Topic ARN. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: preproc is not able to resolve references to deployed resources' physical id + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName" + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change( + self, + snapshot, + capture_update_process, + ): + # Modify the DisplayName of Topic1 from "display-value-1" to "display-value-2" + # while Topic2 references Topic1 using Ref. This verifies that the update process + # correctly reflects the change when using Ref-based dependency resolution. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": "display-value-1", + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": "display-value-2", + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: preproc is not able to resolve references to deployed resources' physical id + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName" + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change_in_ref_chain( + self, + snapshot, + capture_update_process, + ): + # Modify the DisplayName of Topic1 from "display-value-1" to "display-value-2" + # while ensuring that chained references via Ref update appropriately. + # Topic2 references Topic1 using Ref, and Topic3 references Topic2. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + name3 = f"topic-name-3-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + snapshot.add_transformer(RegexTransformer(name3, "topic-name-3")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + "Topic3": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name3, + "DisplayName": {"Ref": "Topic2"}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": "display-value-2", # Updated value triggers change along the chain + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + "Topic3": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name3, + "DisplayName": {"Ref": "Topic2"}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: preproc is not able to resolve references to deployed resources' physical id + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName" + ] + ) + @markers.aws.validated + def test_direct_attribute_value_change_with_dependent_addition( + self, + snapshot, + capture_update_process, + ): + # Modify the DisplayName property of Topic1 while adding Topic2 that + # uses Ref to reference Topic1. + # Initially, only Topic1 exists with DisplayName "display-value-1". + # In the update, Topic1 is updated to "display-value-2" and Topic2 is added, + # referencing Topic1 via Ref. + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-2"}, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) + + # @pytest.mark.skip(reason="") + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: preproc is not able to resolve references to deployed resources' physical id + "$..Changes..ResourceChange.AfterContext.Properties.DisplayName", + # Reason: the preprocessor currently appears to mask the change to the resource as the + # physical id is equal to the logical id. Adding support for physical id resolution + # should address this limitation + "describe-change-set-2..Changes", + "describe-change-set-2-prop-values..Changes", + ] + ) + @markers.aws.validated + def test_immutable_property_update_causes_resource_replacement( + self, + snapshot, + capture_update_process, + ): + # Changing TopicName in Topic1 from an initial value to an updated value + # represents an immutable property update. This forces the replacement of Topic1. + # Topic2 references Topic1 using Ref. After replacement, Topic2's Ref resolution + # should pick up the new Topic1 attributes without error. + name1 = f"topic-name-1-{long_uid()}" + name1_update = f"updated-topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name1_update, "updated-topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": "value", + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1_update, + "DisplayName": "new_value", + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Ref": "Topic1"}, + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json new file mode 100644 index 0000000000000..88caebb48be79 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json @@ -0,0 +1,2444 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_resource_addition": { + "recorded-date": "08-04-2025, 15:22:38", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change": { + "recorded-date": "08-04-2025, 15:36:44", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change_in_ref_chain": { + "recorded-date": "08-04-2025, 15:45:54", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-3" + } + }, + "Details": [], + "LogicalResourceId": "Topic3", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic3", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic3": [ + { + "EventId": "Topic3-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-2", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-3", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-2", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic3-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic3", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-2", + "TopicName": "topic-name-3" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change_with_dependent_addition": { + "recorded-date": "08-04-2025, 15:51:05", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_immutable_property_update_causes_resource_replacement": { + "recorded-date": "08-04-2025, 16:00:20", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "new_value", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "updated-topic-name-1", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "arn::sns::111111111111:topic-name-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "CausingEntity": "Topic1", + "ChangeSource": "ResourceReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "arn::sns::111111111111:topic-name-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Topic1", + "ChangeSource": "ResourceReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-ae91de11-e3e2-4f87-bc72-efe640626413", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-e8338adc-674a-4af1-8430-15ddd3fd7765", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:updated-topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:updated-topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "new_value", + "TopicName": "updated-topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "value", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:updated-topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:updated-topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "arn::sns::111111111111:topic-name-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json new file mode 100644 index 0000000000000..b211c5f80a703 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json @@ -0,0 +1,17 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change": { + "last_validated_date": "2025-04-08T15:36:44+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change_in_ref_chain": { + "last_validated_date": "2025-04-08T15:45:54+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_direct_attribute_value_change_with_dependent_addition": { + "last_validated_date": "2025-04-08T15:51:05+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_immutable_property_update_causes_resource_replacement": { + "last_validated_date": "2025-04-08T16:00:20+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_resource_addition": { + "last_validated_date": "2025-04-08T15:22:37+00:00" + } +} diff --git a/tests/conftest.py b/tests/conftest.py index 6ed59defcd6aa..4a6ae461a9b46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ "localstack.testing.pytest.validation_tracking", "localstack.testing.pytest.path_filter", "localstack.testing.pytest.stepfunctions.fixtures", + "localstack.testing.pytest.cloudformation.fixtures", ] diff --git a/tests/unit/services/cloudformation/test_change_set_describe_details.py b/tests/unit/services/cloudformation/test_change_set_describe_details.py index 00df073c977a6..ea8b79939dfd9 100644 --- a/tests/unit/services/cloudformation/test_change_set_describe_details.py +++ b/tests/unit/services/cloudformation/test_change_set_describe_details.py @@ -3,15 +3,59 @@ import pytest -from localstack.aws.api.cloudformation import ResourceChange +from localstack.aws.api.cloudformation import Changes from localstack.services.cloudformation.engine.v2.change_set_model import ( ChangeSetModel, + ChangeType, + NodeOutput, NodeTemplate, ) from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( ChangeSetModelDescriber, - DescribeUnit, ) +from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( + ChangeSetModelPreproc, + PreprocEntityDelta, + PreprocOutput, +) + + +# TODO: the following is used to debug the logic of the ChangeSetModelPreproc in the +# following temporary test suite. This logic should be removed and the tests on output +# management ported to integration tests using the snapshot strategy. +class DebugOutputPreProc(ChangeSetModelPreproc): + outputs_before: list[dict] + outputs_after: list[dict] + + def __init__(self, node_template: NodeTemplate): + super().__init__(node_template=node_template) + self.outputs_before = list() + self.outputs_after = list() + + @staticmethod + def _to_debug_output(change_type: ChangeType, preproc_output: PreprocOutput) -> dict: + debug_object = { + "ChangeType": change_type.value, + "Name": preproc_output.name, + "Value": preproc_output.value, + } + if preproc_output.condition: + debug_object["Condition"] = preproc_output.condition + if preproc_output.export: + debug_object["Export"] = preproc_output.export + return debug_object + + def visit_node_output( + self, node_output: NodeOutput + ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]: + delta = super().visit_node_output(node_output) + if delta.before: + debug_object = self._to_debug_output(node_output.change_type, delta.before) + self.outputs_before.append(debug_object) + if delta.after: + debug_object = self._to_debug_output(node_output.change_type, delta.after) + self.outputs_after.append(debug_object) + return delta # TODO: this is a temporary test suite for the v2 CFN update engine change set description logic. @@ -23,7 +67,7 @@ def eval_change_set( after_template: dict, before_parameters: Optional[dict] = None, after_parameters: Optional[dict] = None, - ) -> list[ResourceChange]: + ) -> Changes: change_set_model = ChangeSetModel( before_template=before_template, after_template=after_template, @@ -31,19 +75,28 @@ def eval_change_set( after_parameters=after_parameters, ) update_model: NodeTemplate = change_set_model.get_update_model() - change_set_describer = ChangeSetModelDescriber(node_template=update_model) + change_set_describer = ChangeSetModelDescriber( + node_template=update_model, include_property_values=True + ) changes = change_set_describer.get_changes() - # TODO + for change in changes: + resource_change = change["ResourceChange"] + before_context_str = resource_change.get("BeforeContext") + if before_context_str is not None: + resource_change["BeforeContext"] = json.loads(before_context_str) + after_context_str = resource_change.get("AfterContext") + if after_context_str is not None: + resource_change["AfterContext"] = json.loads(after_context_str) json_str = json.dumps(changes) return json.loads(json_str) @staticmethod - def debug_outputs( + def debug_output_preproc( before_template: Optional[dict], after_template: Optional[dict], before_parameters: Optional[dict] = None, after_parameters: Optional[dict] = None, - ) -> DescribeUnit: + ) -> tuple[list[dict], list[dict]]: change_set_model = ChangeSetModel( before_template=before_template, after_template=after_template, @@ -51,8 +104,9 @@ def debug_outputs( after_parameters=after_parameters, ) update_model: NodeTemplate = change_set_model.get_update_model() - outputs_unit = ChangeSetModelDescriber(update_model).visit(update_model.outputs) - return outputs_unit + preproc_output = DebugOutputPreProc(update_model) + preproc_output.visit(update_model.outputs) + return preproc_output.outputs_before, preproc_output.outputs_after @staticmethod def compare_changes(computed: list, target: list) -> None: @@ -1500,11 +1554,13 @@ def test_output_new_resource_and_output(self): }, "Outputs": {"NewParamName": {"Value": {"Ref": "NewParam"}}}, } - outputs_unit = self.debug_outputs(t1, t2) - assert not outputs_unit.before_context + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.after_context == [{"Name": "NewParamName", "Value": "NewParam"}] + # as the executor logic is not yet implemented. + assert not outputs_before + assert outputs_after == [ + {"ChangeType": "Created", "Name": "NewParamName", "Value": "NewParam"} + ] def test_output_and_resource_removed(self): t1 = { @@ -1532,13 +1588,13 @@ def test_output_and_resource_removed(self): } } } - outputs_unit = self.debug_outputs(t1, t2) + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.before_context == [ - {"Name": "FeatureToggleName", "Value": "FeatureToggle"} + # as the executor logic is not yet implemented. + assert outputs_before == [ + {"ChangeType": "Removed", "Name": "FeatureToggleName", "Value": "FeatureToggle"} ] - assert outputs_unit.after_context == [] + assert outputs_after == [] def test_output_resource_changed(self): t1 = { @@ -1567,11 +1623,13 @@ def test_output_resource_changed(self): }, "Outputs": {"LogLevelOutput": {"Value": {"Ref": "LogLevelParam"}}}, } - outputs_unit = self.debug_outputs(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.before_context == [{"Name": "LogLevelOutput", "Value": "LogLevelParam"}] - assert outputs_unit.after_context == [{"Name": "LogLevelOutput", "Value": "LogLevelParam"}] + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) + assert outputs_before == [ + {"ChangeType": "Modified", "Name": "LogLevelOutput", "Value": "LogLevelParam"} + ] + assert outputs_after == [ + {"ChangeType": "Modified", "Name": "LogLevelOutput", "Value": "LogLevelParam"} + ] def test_output_update(self): t1 = { @@ -1601,12 +1659,14 @@ def test_output_update(self): }, "Outputs": {"EnvParamRef": {"Value": {"Fn::GetAtt": ["EnvParam", "Name"]}}}, } - outputs_unit = self.debug_outputs(t1, t2) + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.before_context == [{"Name": "EnvParamRef", "Value": "EnvParam"}] - assert outputs_unit.after_context == [ - {"Name": "EnvParamRef", "Value": "{{changeSet:KNOWN_AFTER_APPLY}}"} + # as the executor logic is not yet implemented. + assert outputs_before == [ + {"ChangeType": "Modified", "Name": "EnvParamRef", "Value": "EnvParam"} + ] + assert outputs_after == [ + {"ChangeType": "Modified", "Name": "EnvParamRef", "Value": "app-env"} ] def test_output_renamed(self): @@ -1636,11 +1696,13 @@ def test_output_renamed(self): }, "Outputs": {"NewSSMOutput": {"Value": {"Ref": "SSMParam"}}}, } - outputs_unit = self.debug_outputs(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.before_context == [{"Name": "OldSSMOutput", "Value": "SSMParam"}] - assert outputs_unit.after_context == [{"Name": "NewSSMOutput", "Value": "SSMParam"}] + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) + assert outputs_before == [ + {"ChangeType": "Removed", "Name": "OldSSMOutput", "Value": "SSMParam"} + ] + assert outputs_after == [ + {"ChangeType": "Created", "Name": "NewSSMOutput", "Value": "SSMParam"} + ] def test_output_and_resource_renamed(self): t1 = { @@ -1669,12 +1731,16 @@ def test_output_and_resource_renamed(self): }, "Outputs": {"DatabaseSecretOutput": {"Value": {"Ref": "DatabaseSecretParam"}}}, } - outputs_unit = self.debug_outputs(t1, t2) + outputs_before, outputs_after = self.debug_output_preproc(t1, t2) # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. This will be moved to the template processor. - assert outputs_unit.before_context == [ - {"Name": "DBPasswordOutput", "Value": "DBPasswordParam"} + # as the executor logic is not yet implemented. + assert outputs_before == [ + {"ChangeType": "Removed", "Name": "DBPasswordOutput", "Value": "DBPasswordParam"} ] - assert outputs_unit.after_context == [ - {"Name": "DatabaseSecretOutput", "Value": "DatabaseSecretParam"} + assert outputs_after == [ + { + "ChangeType": "Created", + "Name": "DatabaseSecretOutput", + "Value": "DatabaseSecretParam", + } ] From 9e033711de883b724e3e891daa8c3c44b7af7977 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:15:34 +0200 Subject: [PATCH 037/108] Step Functions: remove config variables for legacy provided removed in v4.0 (#12492) --- localstack-core/localstack/config.py | 6 ------ localstack-core/localstack/deprecations.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/config.py b/localstack-core/localstack/config.py index 9327053274a18..a5a4aa18b836b 100644 --- a/localstack-core/localstack/config.py +++ b/localstack-core/localstack/config.py @@ -1089,11 +1089,6 @@ def populate_edge_configuration( os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC") or 10 ) -# Adding Stepfunctions default port -LOCAL_PORT_STEPFUNCTIONS = int(os.environ.get("LOCAL_PORT_STEPFUNCTIONS") or 8083) -# Stepfunctions lambda endpoint override -STEPFUNCTIONS_LAMBDA_ENDPOINT = os.environ.get("STEPFUNCTIONS_LAMBDA_ENDPOINT", "").strip() - # path prefix for windows volume mounting WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt") @@ -1364,7 +1359,6 @@ def use_custom_dns(): "SQS_ENDPOINT_STRATEGY", "SQS_DISABLE_CLOUDWATCH_METRICS", "SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL", - "STEPFUNCTIONS_LAMBDA_ENDPOINT", "STRICT_SERVICE_LOADING", "TF_COMPAT_MODE", "USE_SSL", diff --git a/localstack-core/localstack/deprecations.py b/localstack-core/localstack/deprecations.py index 1ece1f5ccfec3..1690ca227d878 100644 --- a/localstack-core/localstack/deprecations.py +++ b/localstack-core/localstack/deprecations.py @@ -311,6 +311,20 @@ def is_affected(self) -> bool: " is faster, achieves great AWS parity, and fixes compatibility issues with the StepFunctions JSONata feature." " Please remove EVENT_RULE_ENGINE.", ), + EnvVarDeprecation( + "STEPFUNCTIONS_LAMBDA_ENDPOINT", + "4.0.0", + "This is only supported for the legacy provider. URL to use as the Lambda service endpoint in Step Functions. " + "By default this is the LocalStack Lambda endpoint. Use default to select the original AWS Lambda endpoint.", + ), + EnvVarDeprecation( + "LOCAL_PORT_STEPFUNCTIONS", + "4.0.0", + "This is only supported for the legacy provider." + "It defines the local port to which Step Functions traffic is redirected." + "By default, LocalStack routes Step Functions traffic to its internal runtime. " + "Use this variable only if you need to redirect traffic to a different local Step Functions runtime.", + ), ] From 755eec9001fc87b88e78a92ff25ca3a26e4b3a1c Mon Sep 17 00:00:00 2001 From: Gentris Leci Date: Wed, 9 Apr 2025 15:22:04 +0200 Subject: [PATCH 038/108] add hyphen to snake-case util function (#12463) --- localstack-core/localstack/utils/strings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/localstack-core/localstack/utils/strings.py b/localstack-core/localstack/utils/strings.py index 33f5f203a3d66..4c0310f4d3e3a 100644 --- a/localstack-core/localstack/utils/strings.py +++ b/localstack-core/localstack/utils/strings.py @@ -78,6 +78,10 @@ def snake_to_camel_case(string: str, capitalize_first: bool = True) -> str: return "".join(components) +def hyphen_to_snake_case(string: str) -> str: + return string.replace("-", "_") + + def canonicalize_bool_to_str(val: bool) -> str: return "true" if str(val).lower() == "true" else "false" From 2ec85e4730ed55f7e21f1e2e88662e1ca390320d Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 9 Apr 2025 13:23:14 +0000 Subject: [PATCH 039/108] Core: Add py.typed, Remove docs from source dist (#12232) --- MANIFEST.in | 6 ++++++ localstack-core/localstack/py.typed | 0 localstack-core/localstack/state/core.py | 12 ++++++------ 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 localstack-core/localstack/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index 2afd2693472a3..07442c11a993f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,10 @@ +exclude .github/** +exclude .circleci/** +exclude docs/** exclude tests/** exclude .test_durations +exclude .gitignore +exclude .pre-commit-config.yaml +exclude .python-version include Makefile include LICENSE.txt diff --git a/localstack-core/localstack/py.typed b/localstack-core/localstack/py.typed new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack-core/localstack/state/core.py b/localstack-core/localstack/state/core.py index aa27a84fc843e..ae41f47b17469 100644 --- a/localstack-core/localstack/state/core.py +++ b/localstack-core/localstack/state/core.py @@ -27,27 +27,27 @@ class StateLifecycleHook: - load: the state is injected into the service, or state directories on disk are restored """ - def on_before_state_reset(self): + def on_before_state_reset(self) -> None: """Hook triggered before the provider's state containers are reset/cleared.""" pass - def on_after_state_reset(self): + def on_after_state_reset(self) -> None: """Hook triggered after the provider's state containers have been reset/cleared.""" pass - def on_before_state_save(self): + def on_before_state_save(self) -> None: """Hook triggered before the provider's state containers are saved.""" pass - def on_after_state_save(self): + def on_after_state_save(self) -> None: """Hook triggered after the provider's state containers have been saved.""" pass - def on_before_state_load(self): + def on_before_state_load(self) -> None: """Hook triggered before a previously serialized state is loaded into the provider's state containers.""" pass - def on_after_state_load(self): + def on_after_state_load(self) -> None: """Hook triggered after a previously serialized state has been loaded into the provider's state containers.""" pass From 69d7a75fc115963dd0420aa43314287df12d3100 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:43:21 +0200 Subject: [PATCH 040/108] Step Functions Mocking: Add Support for Mounting Mocked Service Responses via MockConfigFile.json (#12493) --- localstack-core/localstack/config.py | 3 + .../stepfunctions/mocking/__init__.py | 0 .../stepfunctions/mocking/mock_config.py | 69 +++++++++++++++++++ .../testing/pytest/stepfunctions/fixtures.py | 31 +++++++++ .../mocked_responses/__init__.py | 0 .../mocked_response_loader.py | 24 +++++++ .../mocked_responses/__init__.py | 0 .../lambda/200_string_body.json5 | 11 +++ .../mocked_responses/lambda/__init__.py | 0 .../stepfunctions/v2/mocking/__init__.py | 0 .../v2/mocking/test_mock_config_file.py | 28 ++++++++ 11 files changed, 166 insertions(+) create mode 100644 localstack-core/localstack/services/stepfunctions/mocking/__init__.py create mode 100644 localstack-core/localstack/services/stepfunctions/mocking/mock_config.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/__init__.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/__init__.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py diff --git a/localstack-core/localstack/config.py b/localstack-core/localstack/config.py index a5a4aa18b836b..f4470356b7130 100644 --- a/localstack-core/localstack/config.py +++ b/localstack-core/localstack/config.py @@ -1089,6 +1089,9 @@ def populate_edge_configuration( os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC") or 10 ) +# Specifies the path to the mock configuration file for Step Functions, commonly named MockConfigFile.json. +SFN_MOCK_CONFIG = os.environ.get("SFN_MOCK_CONFIG", "").strip() + # path prefix for windows volume mounting WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt") diff --git a/localstack-core/localstack/services/stepfunctions/mocking/__init__.py b/localstack-core/localstack/services/stepfunctions/mocking/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py new file mode 100644 index 0000000000000..f76b80c358b96 --- /dev/null +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py @@ -0,0 +1,69 @@ +import json +import logging +import os +from functools import lru_cache +from typing import Optional + +from localstack import config + +LOG = logging.getLogger(__name__) + + +@lru_cache(maxsize=1) +def _retrieve_sfn_mock_config(file_path: str, modified_epoch: int) -> Optional[dict]: # noqa + """ + Load and cache the Step Functions mock configuration from a JSON file. + + This function is memoized using `functools.lru_cache` to avoid re-reading the file + from disk unless it has changed. The `modified_epoch` parameter is used solely to + trigger cache invalidation when the file is updated. If either the file path or the + modified timestamp changes, the cached result is discarded and the file is reloaded. + + Parameters: + file_path (str): + The absolute path to the JSON configuration file. + + modified_epoch (int): + The last modified time of the file, in epoch seconds. This value is used + as part of the cache key to ensure the cache is refreshed when the file is updated. + + Returns: + Optional[dict]: + The parsed configuration as a dictionary if the file is successfully loaded, + or `None` if an error occurs during reading or parsing. + + Notes: + - The `modified_epoch` argument is not used inside the function logic, but is + necessary to ensure cache correctness via `lru_cache`. + - Logging is used to capture warnings if file access or parsing fails. + """ + try: + with open(file_path, "r") as df: + mock_config = json.load(df) + return mock_config + except Exception as ex: + LOG.warning( + "Unable to load step functions mock configuration file at '%s' due to %s", + file_path, + ex, + ) + return None + + +def load_sfn_mock_config_file() -> Optional[dict]: + configuration_file_path = config.SFN_MOCK_CONFIG + if not configuration_file_path: + return None + + try: + modified_time = int(os.path.getmtime(configuration_file_path)) + except Exception as ex: + LOG.warning( + "Unable to access the step functions mock configuration file at '%s' due to %s", + configuration_file_path, + ex, + ) + return None + + mock_config = _retrieve_sfn_mock_config(configuration_file_path, modified_time) + return mock_config diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py index 0fdcfbebdfad7..13a134d269e85 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py @@ -1,5 +1,8 @@ import json import logging +import os +import shutil +import tempfile from typing import Final import pytest @@ -144,6 +147,34 @@ def aws_client_no_sync_prefix(aws_client_factory): return aws_client_factory(config=Config(inject_host_prefix=is_aws_cloud())) +@pytest.fixture +def mock_config_file(): + tmp_dir = tempfile.mkdtemp() + file_path = os.path.join(tmp_dir, "MockConfigFile.json") + + def write_json_to_mock_file(mock_config): + with open(file_path, "w") as df: + json.dump(mock_config, df) # noqa + df.flush() + return file_path + + try: + yield write_json_to_mock_file + finally: + try: + os.remove(file_path) + except Exception as ex: + LOG.error("Error removing temporary MockConfigFile.json: %s", ex) + finally: + shutil.rmtree( + tmp_dir, + ignore_errors=True, + onerror=lambda _, path, exc_info: LOG.error( + "Error removing temporary MockConfigFile.json: %s, %s", path, exc_info + ), + ) + + @pytest.fixture def create_state_machine_iam_role(cleanups, create_state_machine): def _create(target_aws_client): diff --git a/tests/aws/services/stepfunctions/mocked_responses/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py new file mode 100644 index 0000000000000..032b72f5361b7 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py @@ -0,0 +1,24 @@ +import abc +import copy +import os +from typing import Final + +import json5 + +_THIS_FOLDER: Final[str] = os.path.dirname(os.path.realpath(__file__)) +_LOAD_CACHE: Final[dict[str, dict]] = dict() + + +class MockedResponseLoader(abc.ABC): + LAMBDA_200_STRING_BODY: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/lambda/200_string_body.json5" + ) + + @staticmethod + def load(file_path: str) -> dict: + template = _LOAD_CACHE.get(file_path) + if template is None: + with open(file_path, "r") as df: + template = json5.load(df) + _LOAD_CACHE[file_path] = template + return copy.deepcopy(template) diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 new file mode 100644 index 0000000000000..895c7c06d59eb --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 @@ -0,0 +1,11 @@ +{ + "0": { + "Return": { + "StatusCode": 200, + "Payload": { + "StatusCode": 200, + "body": "string body" + } + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/v2/mocking/__init__.py b/tests/aws/services/stepfunctions/v2/mocking/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py new file mode 100644 index 0000000000000..7fa346acec8ea --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py @@ -0,0 +1,28 @@ +from localstack import config +from localstack.services.stepfunctions.mocking.mock_config import load_sfn_mock_config_file +from localstack.testing.pytest import markers +from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( + MockedResponseLoader, +) + + +class TestMockConfigFile: + @markers.aws.only_localstack + def test_is_mock_config_flag_detected_unset(self, mock_config_file): + loaded_mock_config_file = load_sfn_mock_config_file() + assert loaded_mock_config_file is None + + @markers.aws.only_localstack + def test_is_mock_config_flag_detected_set(self, mock_config_file, monkeypatch): + lambda_200_string_body = MockedResponseLoader.load( + MockedResponseLoader.LAMBDA_200_STRING_BODY + ) + # TODO: add typing for MockConfigFile.json components + mock_config = { + "StateMachines": {"S0": {"TestCases": {"LambdaState": "lambda_200_string_body"}}}, + "MockedResponses": {"lambda_200_string_body": lambda_200_string_body}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + loaded_mock_config_file = load_sfn_mock_config_file() + assert loaded_mock_config_file == mock_config From 163790cf1578bc4cb537047da1c565f924d5984e Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Wed, 9 Apr 2025 20:07:14 +0100 Subject: [PATCH 041/108] EC2: generate security group ids using id manager concept (#12494) Co-authored-by: Mathieu Cloutier --- .../localstack/services/ec2/patches.py | 37 +++++++- tests/aws/services/ec2/test_ec2.py | 93 +++++++++++-------- 2 files changed, 86 insertions(+), 44 deletions(-) diff --git a/localstack-core/localstack/services/ec2/patches.py b/localstack-core/localstack/services/ec2/patches.py index d26d94a3df83b..d2037015905ef 100644 --- a/localstack-core/localstack/services/ec2/patches.py +++ b/localstack-core/localstack/services/ec2/patches.py @@ -2,7 +2,7 @@ from typing import Optional from moto.ec2 import models as ec2_models -from moto.utilities.id_generator import TAG_KEY_CUSTOM_ID, Tags +from moto.utilities.id_generator import Tags from localstack.services.ec2.exceptions import ( InvalidSecurityGroupDuplicateCustomIdError, @@ -29,6 +29,16 @@ def generate_vpc_id( return "" +@localstack_id +def generate_security_group_id( + resource_identifier: ResourceIdentifier, + existing_ids: ExistingIds = None, + tags: Tags = None, +) -> str: + # We return an empty string here to differentiate between when a custom ID was used, or when it was randomly generated by `moto`. + return "" + + @localstack_id def generate_subnet_id( resource_identifier: ResourceIdentifier, @@ -54,6 +64,19 @@ def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str: ) +class SecurityGroupIdentifier(ResourceIdentifier): + service = "ec2" + resource = "securitygroup" + + def __init__(self, account_id: str, region: str, vpc_id: str, group_name: str): + super().__init__(account_id, region, name=f"sg-{vpc_id}-{group_name}") + + def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str: + return generate_security_group_id( + resource_identifier=self, existing_ids=existing_ids, tags=tags + ) + + class SubnetIdentifier(ResourceIdentifier): service = "ec2" resource = "subnet" @@ -126,21 +149,25 @@ def ec2_create_subnet( def ec2_create_security_group( fn: ec2_models.security_groups.SecurityGroupBackend.create_security_group, self: ec2_models.security_groups.SecurityGroupBackend, + name: str, *args, + vpc_id: Optional[str] = None, tags: Optional[dict[str, str]] = None, force: bool = False, **kwargs, ): - # Extract tags and custom ID - tags: dict[str, str] = tags or {} - custom_id = tags.get(TAG_KEY_CUSTOM_ID) + vpc_id = vpc_id or self.default_vpc.id + resource_identifier = SecurityGroupIdentifier( + self.account_id, self.region_name, vpc_id, name + ) + custom_id = resource_identifier.generate(tags=tags) if not force and self.get_security_group_from_id(custom_id): raise InvalidSecurityGroupDuplicateCustomIdError(custom_id) # Generate security group with moto library result: ec2_models.security_groups.SecurityGroup = fn( - self, *args, tags=tags, force=force, **kwargs + self, name, *args, vpc_id=vpc_id, tags=tags, force=force, **kwargs ) if custom_id: diff --git a/tests/aws/services/ec2/test_ec2.py b/tests/aws/services/ec2/test_ec2.py index 5ef1a8c15d743..f0ba136034454 100644 --- a/tests/aws/services/ec2/test_ec2.py +++ b/tests/aws/services/ec2/test_ec2.py @@ -11,8 +11,9 @@ ) from localstack.constants import TAG_KEY_CUSTOM_ID -from localstack.services.ec2.patches import VpcIdentifier +from localstack.services.ec2.patches import SecurityGroupIdentifier, VpcIdentifier from localstack.testing.pytest import markers +from localstack.utils.id_generator import localstack_id_manager from localstack.utils.strings import short_uid from localstack.utils.sync import retry @@ -618,30 +619,57 @@ def test_create_subnet_with_custom_id_and_vpc_id(self, cleanups, aws_client, cre assert subnet["Tags"][0]["Value"] == custom_subnet_id @markers.aws.only_localstack - def test_create_security_group_with_custom_id(self, cleanups, aws_client, create_vpc): + @pytest.mark.parametrize("strategy", ["tag", "id_manager"]) + @pytest.mark.parametrize("default_vpc", [True, False]) + def test_create_security_group_with_custom_id( + self, cleanups, aws_client, create_vpc, strategy, account_id, region_name, default_vpc + ): custom_id = random_security_group_id() + group_name = f"test-security-group-{short_uid()}" + vpc_id = None # Create necessary VPC resource - vpc: dict = create_vpc( - cidr_block="10.0.0.0/24", - tag_specifications=[], - ) + if default_vpc: + vpc: dict = aws_client.ec2.describe_vpcs( + Filters=[{"Name": "is-default", "Values": ["true"]}] + )["Vpcs"][0] + vpc_id = vpc["VpcId"] + else: + vpc: dict = create_vpc( + cidr_block="10.0.0.0/24", + tag_specifications=[], + ) + vpc_id = vpc["Vpc"]["VpcId"] + + def _create_security_group() -> dict: + req_kwargs = {"Description": "Test security group", "GroupName": group_name} + if not default_vpc: + # vpc_id does not need to be provided for default vpc + req_kwargs["VpcId"] = vpc_id + if strategy == "tag": + req_kwargs["TagSpecifications"] = [ + { + "ResourceType": "security-group", + "Tags": [{"Key": TAG_KEY_CUSTOM_ID, "Value": custom_id}], + } + ] + return aws_client.ec2.create_security_group(**req_kwargs) + else: + with localstack_id_manager.custom_id( + SecurityGroupIdentifier( + account_id=account_id, + region=region_name, + vpc_id=vpc_id, + group_name=group_name, + ), + custom_id, + ): + return aws_client.ec2.create_security_group(**req_kwargs) + + security_group: dict = _create_security_group() - # Check if security group ID matches the custom ID - security_group: dict = aws_client.ec2.create_security_group( - Description="Test security group", - GroupName="test-security-group-0", - VpcId=vpc["Vpc"]["VpcId"], - TagSpecifications=[ - { - "ResourceType": "security-group", - "Tags": [ - {"Key": TAG_KEY_CUSTOM_ID, "Value": custom_id}, - ], - } - ], - ) cleanups.append(lambda: aws_client.ec2.delete_security_group(GroupId=custom_id)) + # Check if security group ID matches the custom ID assert security_group["GroupId"] == custom_id, ( f"Security group ID does not match custom ID: {security_group}" ) @@ -652,29 +680,16 @@ def test_create_security_group_with_custom_id(self, cleanups, aws_client, create )["SecurityGroups"] # Get security group that match a given VPC id - security_group = next( - (sg for sg in security_groups if sg["VpcId"] == vpc["Vpc"]["VpcId"]), None - ) + security_group = next((sg for sg in security_groups if sg["VpcId"] == vpc_id), None) assert security_group["GroupId"] == custom_id - assert len(security_group["Tags"]) == 1 - assert security_group["Tags"][0]["Key"] == TAG_KEY_CUSTOM_ID - assert security_group["Tags"][0]["Value"] == custom_id + if strategy == "tag": + assert len(security_group["Tags"]) == 1 + assert security_group["Tags"][0]["Key"] == TAG_KEY_CUSTOM_ID + assert security_group["Tags"][0]["Value"] == custom_id # Check if a duplicate custom ID exception is thrown if we try to recreate the security group with the same custom ID with pytest.raises(ClientError) as e: - aws_client.ec2.create_security_group( - Description="Test security group", - GroupName="test-security-group-1", - VpcId=vpc["Vpc"]["VpcId"], - TagSpecifications=[ - { - "ResourceType": "security-group", - "Tags": [ - {"Key": TAG_KEY_CUSTOM_ID, "Value": custom_id}, - ], - } - ], - ) + _create_security_group() assert e.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400 assert e.value.response["Error"]["Code"] == "InvalidSecurityGroupId.DuplicateCustomId" From 587ffca99f5da3fac86ee1bba0e6d9bbde285162 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:56:00 +0530 Subject: [PATCH 042/108] KMS: enable `_custom_key_material_` for ECC keys (#12504) --- .../localstack/services/kms/models.py | 5 +- tests/aws/services/kms/test_kms.py | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/services/kms/models.py b/localstack-core/localstack/services/kms/models.py index 91f62a542ec07..6f91ada1c923d 100644 --- a/localstack-core/localstack/services/kms/models.py +++ b/localstack-core/localstack/services/kms/models.py @@ -196,7 +196,10 @@ def __init__(self, key_spec: str, key_material: Optional[bytes] = None): key = rsa.generate_private_key(public_exponent=65537, key_size=key_size) elif key_spec.startswith("ECC"): curve = ECC_CURVES.get(key_spec) - key = ec.generate_private_key(curve) + if key_material: + key = crypto_serialization.load_der_private_key(key_material, password=None) + else: + key = ec.generate_private_key(curve) elif key_spec.startswith("HMAC"): if key_spec not in HMAC_RANGE_KEY_LENGTHS: raise ValidationException( diff --git a/tests/aws/services/kms/test_kms.py b/tests/aws/services/kms/test_kms.py index bd9b5eab08cc3..23b52722e6326 100644 --- a/tests/aws/services/kms/test_kms.py +++ b/tests/aws/services/kms/test_kms.py @@ -377,6 +377,53 @@ def test_create_key_custom_key_material_symmetric_decrypt(self, kms_create_key, )["Plaintext"] assert plaintext == message + @markers.aws.only_localstack + def test_create_custom_key_asymmetric(self, kms_create_key, aws_client): + crypto_key = ec.generate_private_key(ec.SECP256K1()) + raw_private_key = crypto_key.private_bytes( + serialization.Encoding.DER, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + raw_public_key = crypto_key.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + custom_key_material = raw_private_key + + custom_key_tag_value = base64.b64encode(custom_key_material).decode("utf-8") + + key_spec = "ECC_SECG_P256K1" + key_usage = "SIGN_VERIFY" + + key_id = kms_create_key( + Tags=[{"TagKey": "_custom_key_material_", "TagValue": custom_key_tag_value}], + KeySpec=key_spec, + KeyUsage=key_usage, + )["KeyId"] + + public_key = aws_client.kms.get_public_key(KeyId=key_id)["PublicKey"] + + assert public_key == raw_public_key + + # Do a sign/verify cycle + plaintext = b"test message 123 !%$@ 1234567890" + + signature = crypto_key.sign( + plaintext, + ec.ECDSA(hashes.SHA256()), + ) + + verify_data = aws_client.kms.verify( + Message=plaintext, + Signature=signature, + MessageType="RAW", + SigningAlgorithm="ECDSA_SHA_256", + KeyId=key_id, + ) + assert verify_data["SignatureValid"] + @markers.aws.validated def test_get_key_in_different_region( self, kms_client_for_region, kms_create_key, snapshot, region_name, secondary_region_name From f1ef65a197fb0789b03f78759b35795d084d48e0 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Thu, 10 Apr 2025 08:37:01 +0200 Subject: [PATCH 043/108] unpin pytest-httpserver, fix patches (#12507) --- pyproject.toml | 3 +-- requirements-dev.txt | 2 +- requirements-test.txt | 2 +- requirements-typehint.txt | 2 +- tests/aws/services/events/test_events_targets.py | 2 +- tests/conftest.py | 14 +++++++++++--- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 94eb3a2e67cdd..8ac9fb745ce19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,8 +108,7 @@ test = [ "pluggy>=1.3.0", "pytest>=7.4.2", "pytest-split>=0.8.0", - # TODO fix issues with pytest-httpserver==1.1.2, remove upper boundary - "pytest-httpserver>=1.0.1,<1.1.2", + "pytest-httpserver>=1.1.2", "pytest-rerunfailures>=12.0", "pytest-tinybird>=0.2.0", "aws-cdk-lib>=2.88.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 011b12d670d12..bd777d171521a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -355,7 +355,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.1 +pytest-httpserver==1.1.2 # via localstack-core pytest-rerunfailures==15.0 # via localstack-core diff --git a/requirements-test.txt b/requirements-test.txt index 27acf9dd37d94..d51792859d9a2 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -323,7 +323,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.1 +pytest-httpserver==1.1.2 # via localstack-core (pyproject.toml) pytest-rerunfailures==15.0 # via localstack-core (pyproject.toml) diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 5c4bd196e2c46..c73806f88c658 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -565,7 +565,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.1 +pytest-httpserver==1.1.2 # via localstack-core pytest-rerunfailures==15.0 # via localstack-core diff --git a/tests/aws/services/events/test_events_targets.py b/tests/aws/services/events/test_events_targets.py index 10ca5d35f2790..a4c641466f4d2 100644 --- a/tests/aws/services/events/test_events_targets.py +++ b/tests/aws/services/events/test_events_targets.py @@ -163,7 +163,7 @@ def _handler(_request: Request): clean_up(rule_name=rule_name, target_ids=target_id) to_recv = 2 if auth["type"] == "OAUTH_CLIENT_CREDENTIALS" else 1 - poll_condition(lambda: len(httpserver.log) >= to_recv, timeout=5) + assert poll_condition(lambda: len(httpserver.log) >= to_recv, timeout=5) event_request, _ = httpserver.log[-1] event = event_request.get_json(force=True) diff --git a/tests/conftest.py b/tests/conftest.py index 4a6ae461a9b46..5b9e1edde8f1f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,23 +20,31 @@ ] -# FIXME: remove this, quick hack to prevent the HTTPServer fixture to spawn non-daemon threads +# FIXME: remove this once https://github.com/csernazs/pytest-httpserver/pull/411 is merged def pytest_sessionstart(session): import threading try: from pytest_httpserver import HTTPServer, HTTPServerError + from werkzeug import Request from werkzeug.serving import make_server from localstack.utils.patch import Patch - def start_non_daemon_thread(self): + def start_non_daemon_thread(self) -> None: if self.is_running(): raise HTTPServerError("Server is already running") + app = Request.application(self.application) + self.server = make_server( - self.host, self.port, self.application, ssl_context=self.ssl_context + self.host, + self.port, + app, + ssl_context=self.ssl_context, + threaded=self.threaded, ) + self.port = self.server.port # Update port (needed if `port` was set to 0) self.server_thread = threading.Thread(target=self.thread_target, daemon=True) self.server_thread.start() From 073eab9351889be7a3699e29b67d8ea469e986bf Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:33:51 +0530 Subject: [PATCH 044/108] skip flaky tests in transcribe (#12509) --- tests/aws/services/transcribe/test_transcribe.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/aws/services/transcribe/test_transcribe.py b/tests/aws/services/transcribe/test_transcribe.py index d52ac48c7e886..67c24351a8a88 100644 --- a/tests/aws/services/transcribe/test_transcribe.py +++ b/tests/aws/services/transcribe/test_transcribe.py @@ -92,6 +92,7 @@ def transcribe_snapshot_transformer(snapshot): snapshot.add_transformer(snapshot.transform.transcribe_api()) +@pytest.mark.skip(reason="flaky") class TestTranscribe: @pytest.fixture(scope="class", autouse=True) def pre_install_dependencies(self): @@ -138,7 +139,6 @@ def is_transcription_done(): "$..Error..Code", ] ) - @pytest.mark.skip(reason="flaky") def test_transcribe_happy_path(self, transcribe_create_job, snapshot, aws_client): file_path = os.path.join(BASEDIR, "../../files/en-gb.wav") job_name = transcribe_create_job(audio_file=file_path) @@ -183,7 +183,6 @@ def is_transcription_done(): ], ) @markers.aws.needs_fixing - @pytest.mark.skip(reason="flaky") def test_transcribe_supported_media_formats( self, transcribe_create_job, media_file, speech, aws_client ): @@ -324,7 +323,6 @@ def test_failing_start_transcription_job(self, s3_bucket, snapshot, aws_client): (None, None), # without output bucket and output key ], ) - @pytest.mark.skip(reason="flaky") def test_transcribe_start_job( self, output_bucket, From b619dedbf4d4e2d5f61181739bd52ce5be084240 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:05:21 +0530 Subject: [PATCH 045/108] transcribe: add model path to vosk Model (#12479) --- .../localstack/services/transcribe/provider.py | 17 +++++++++-------- .../aws/services/transcribe/test_transcribe.py | 1 - 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/localstack-core/localstack/services/transcribe/provider.py b/localstack-core/localstack/services/transcribe/provider.py index f33f3c4b1013a..29fbdf6a552e1 100644 --- a/localstack-core/localstack/services/transcribe/provider.py +++ b/localstack-core/localstack/services/transcribe/provider.py @@ -1,7 +1,6 @@ import datetime import json import logging -import os import threading import wave from functools import cache @@ -124,8 +123,6 @@ def _setup_vosk() -> None: # Install and configure vosk vosk_package.install() - # Vosk must be imported only after setting the required env vars - os.environ["VOSK_MODEL_PATH"] = str(LANGUAGE_MODEL_DIR) from vosk import SetLogLevel # noqa # Suppress Vosk logging @@ -230,7 +227,7 @@ def delete_transcription_job( # @staticmethod - def download_model(name: str): + def download_model(name: str) -> str: """ Download a Vosk language model to LocalStack cache directory. Do nothing if model is already downloaded. @@ -240,8 +237,10 @@ def download_model(name: str): model_path = LANGUAGE_MODEL_DIR / name with _DL_LOCK: - if model_path.exists(): - return + # check if model path exists and is not empty + if model_path.exists() and any(model_path.iterdir()): + LOG.debug("Using a pre-downloaded language model: %s", model_path) + return str(model_path) else: model_path.mkdir(parents=True) @@ -267,6 +266,8 @@ def download_model(name: str): Path(model_zip_path).unlink() + return str(model_path) + # # Threads # @@ -338,10 +339,10 @@ def _run_transcription_job(self, args: Tuple[TranscribeStore, str]): language_code = job["LanguageCode"] model_name = LANGUAGE_MODELS[language_code] self._setup_vosk() - self.download_model(model_name) + model_path = self.download_model(model_name) from vosk import KaldiRecognizer, Model # noqa - model = Model(model_name=model_name) + model = Model(model_path=model_path, model_name=model_name) tc = KaldiRecognizer(model, audio.getframerate()) tc.SetWords(True) diff --git a/tests/aws/services/transcribe/test_transcribe.py b/tests/aws/services/transcribe/test_transcribe.py index 67c24351a8a88..572b1b0a4c0b1 100644 --- a/tests/aws/services/transcribe/test_transcribe.py +++ b/tests/aws/services/transcribe/test_transcribe.py @@ -92,7 +92,6 @@ def transcribe_snapshot_transformer(snapshot): snapshot.add_transformer(snapshot.transform.transcribe_api()) -@pytest.mark.skip(reason="flaky") class TestTranscribe: @pytest.fixture(scope="class", autouse=True) def pre_install_dependencies(self): From ab2d6a4ec7afd63a42f2343e9ef3108ed14de437 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:59:44 +0200 Subject: [PATCH 046/108] Step Functions: Increase Retry Attempts on Service Integrations for Resilience Against Transient Network Errors (#12512) --- .../services/stepfunctions/asl/utils/boto_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py b/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py index c7facf1bb532c..e2f17cba8a54b 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py +++ b/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py @@ -10,7 +10,10 @@ _BOTO_CLIENT_CONFIG = config = Config( parameter_validation=False, - retries={"total_max_attempts": 1}, + # Temporary workaround—should be reverted once underlying potential Lambda limitation is resolved. + # Increased total boto client retry attempts from 1 to 5 to mitigate transient service issues. + # This helps reduce unnecessary state machine retries on non-service-level errors. + retries={"total_max_attempts": 5}, connect_timeout=TimeoutSeconds.DEFAULT_TIMEOUT_SECONDS, read_timeout=TimeoutSeconds.DEFAULT_TIMEOUT_SECONDS, tcp_keepalive=True, From 43ad71bb6845a19eb2c9730b069ed7461742f0ed Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 11 Apr 2025 14:24:43 +0200 Subject: [PATCH 047/108] SES: add ARN utility for SES identity (#12513) --- .../localstack/testing/pytest/fixtures.py | 24 +++++++++++++++++++ localstack-core/localstack/utils/aws/arns.py | 10 ++++++++ tests/aws/services/ses/test_ses.py | 24 ------------------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/fixtures.py b/localstack-core/localstack/testing/pytest/fixtures.py index 66cc5c2f016eb..a127e9a94aab5 100644 --- a/localstack-core/localstack/testing/pytest/fixtures.py +++ b/localstack-core/localstack/testing/pytest/fixtures.py @@ -1973,6 +1973,30 @@ def factory(email_address: str) -> None: aws_client.ses.delete_identity(Identity=identity) +@pytest.fixture +def setup_sender_email_address(ses_verify_identity): + """ + If the test is running against AWS then assume the email address passed is already + verified, and passes the given email address through. Otherwise, it generates one random + email address and verify them. + """ + + def inner(sender_email_address: Optional[str] = None) -> str: + if is_aws_cloud(): + if sender_email_address is None: + raise ValueError( + "sender_email_address must be specified to run this test against AWS" + ) + else: + # overwrite the given parameters with localstack specific ones + sender_email_address = f"sender-{short_uid()}@example.com" + ses_verify_identity(sender_email_address) + + return sender_email_address + + return inner + + @pytest.fixture def ec2_create_security_group(aws_client): ec2_sgs = [] diff --git a/localstack-core/localstack/utils/aws/arns.py b/localstack-core/localstack/utils/aws/arns.py index 6caf2d10a6c5e..5b6f139473bac 100644 --- a/localstack-core/localstack/utils/aws/arns.py +++ b/localstack-core/localstack/utils/aws/arns.py @@ -524,6 +524,16 @@ def route53_resolver_query_log_config_arn(id: str, account_id: str, region_name: return _resource_arn(id, pattern, account_id=account_id, region_name=region_name) +# +# SES +# + + +def ses_identity_arn(email: str, account_id: str, region_name: str) -> str: + pattern = "arn:%s:ses:%s:%s:identity/%s" + return _resource_arn(email, pattern, account_id=account_id, region_name=region_name) + + # # Other ARN related helpers # diff --git a/tests/aws/services/ses/test_ses.py b/tests/aws/services/ses/test_ses.py index 0631d8e94ff15..126edfc717ded 100644 --- a/tests/aws/services/ses/test_ses.py +++ b/tests/aws/services/ses/test_ses.py @@ -116,30 +116,6 @@ def inner( return inner -@pytest.fixture -def setup_sender_email_address(ses_verify_identity): - """ - If the test is running against AWS then assume the email address passed is already - verified, and passes the given email address through. Otherwise, it generates one random - email address and verify them. - """ - - def inner(sender_email_address: Optional[str] = None) -> str: - if is_aws_cloud(): - if sender_email_address is None: - raise ValueError( - "sender_email_address must be specified to run this test against AWS" - ) - else: - # overwrite the given parameters with localstack specific ones - sender_email_address = f"sender-{short_uid()}@example.com" - ses_verify_identity(sender_email_address) - - return sender_email_address - - return inner - - @pytest.fixture def add_snapshot_transformer_for_sns_event(snapshot): def _inner(sender_email, recipient_email, config_set_name): From a1eec509578df98d1d4698f584e5a3e37b3acf06 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Fri, 11 Apr 2025 21:14:36 +0200 Subject: [PATCH 048/108] APIGW: migrate TestInvokeMethod to NextGen (#12514) --- .../localstack/services/apigateway/helpers.py | 31 --- .../services/apigateway/legacy/provider.py | 8 +- .../next_gen/execute_api/test_invoke.py | 206 ++++++++++++++++++ .../services/apigateway/next_gen/provider.py | 25 ++- .../apigateway/apigateway_fixtures.py | 102 --------- tests/aws/services/apigateway/conftest.py | 3 +- .../apigateway/test_apigateway_api.py | 1 + .../apigateway/test_apigateway_basic.py | 143 ++++++++---- .../test_apigateway_basic.snapshot.json | 58 ++++- .../test_apigateway_basic.validation.json | 2 +- 10 files changed, 390 insertions(+), 189 deletions(-) create mode 100644 localstack-core/localstack/services/apigateway/next_gen/execute_api/test_invoke.py diff --git a/localstack-core/localstack/services/apigateway/helpers.py b/localstack-core/localstack/services/apigateway/helpers.py index cde25c4bdaba2..aeb6eed73073a 100644 --- a/localstack-core/localstack/services/apigateway/helpers.py +++ b/localstack-core/localstack/services/apigateway/helpers.py @@ -3,7 +3,6 @@ import hashlib import json import logging -from datetime import datetime from typing import List, Optional, TypedDict, Union from urllib import parse as urlparse @@ -61,7 +60,6 @@ {formatted_date} : Method completed with status: {status_code} """ - EMPTY_MODEL = "Empty" ERROR_MODEL = "Error" @@ -984,35 +982,6 @@ def is_variable_path(path_part: str) -> bool: return path_part.startswith("{") and path_part.endswith("}") -def log_template( - request_id: str, - date: datetime, - http_method: str, - resource_path: str, - request_path: str, - query_string: str, - request_headers: str, - request_body: str, - response_body: str, - response_headers: str, - status_code: str, -): - formatted_date = date.strftime("%a %b %d %H:%M:%S %Z %Y") - return INVOKE_TEST_LOG_TEMPLATE.format( - request_id=request_id, - formatted_date=formatted_date, - http_method=http_method, - resource_path=resource_path, - request_path=request_path, - query_string=query_string, - request_headers=request_headers, - request_body=request_body, - response_body=response_body, - response_headers=response_headers, - status_code=status_code, - ) - - def get_domain_name_hash(domain_name: str) -> str: """ Return a hash of the given domain name, which help construct regional domain names for APIs. diff --git a/localstack-core/localstack/services/apigateway/legacy/provider.py b/localstack-core/localstack/services/apigateway/legacy/provider.py index 996e9d170dc1a..ecdab2873a7bd 100644 --- a/localstack-core/localstack/services/apigateway/legacy/provider.py +++ b/localstack-core/localstack/services/apigateway/legacy/provider.py @@ -98,6 +98,7 @@ from localstack.services.apigateway.helpers import ( EMPTY_MODEL, ERROR_MODEL, + INVOKE_TEST_LOG_TEMPLATE, OpenAPIExt, apply_json_patch_safe, get_apigateway_store, @@ -108,7 +109,6 @@ import_api_from_openapi_spec, is_greedy_path, is_variable_path, - log_template, resolve_references, ) from localstack.services.apigateway.legacy.helpers import multi_value_dict_for_list @@ -217,9 +217,10 @@ def test_invoke_method( # TODO: add the missing fields to the log. Next iteration will add helpers to extract the missing fields # from the apicontext - log = log_template( + formatted_date = req_start_time.strftime("%a %b %d %H:%M:%S %Z %Y") + log = INVOKE_TEST_LOG_TEMPLATE.format( request_id=invocation_context.context["requestId"], - date=req_start_time, + formatted_date=formatted_date, http_method=invocation_context.method, resource_path=invocation_context.invocation_path, request_path="", @@ -230,6 +231,7 @@ def test_invoke_method( response_headers=result.headers, status_code=result.status_code, ) + return TestInvokeMethodResponse( status=result.status_code, headers=dict(result.headers), diff --git a/localstack-core/localstack/services/apigateway/next_gen/execute_api/test_invoke.py b/localstack-core/localstack/services/apigateway/next_gen/execute_api/test_invoke.py new file mode 100644 index 0000000000000..4ed1a4c0db845 --- /dev/null +++ b/localstack-core/localstack/services/apigateway/next_gen/execute_api/test_invoke.py @@ -0,0 +1,206 @@ +import datetime +from urllib.parse import parse_qs + +from rolo import Request +from rolo.gateway.chain import HandlerChain +from werkzeug.datastructures import Headers + +from localstack.aws.api.apigateway import TestInvokeMethodRequest, TestInvokeMethodResponse +from localstack.constants import APPLICATION_JSON +from localstack.http import Response +from localstack.utils.strings import to_bytes, to_str + +from ...models import RestApiDeployment +from . import handlers +from .context import InvocationRequest, RestApiInvocationContext +from .handlers.resource_router import RestAPIResourceRouter +from .header_utils import build_multi_value_headers +from .template_mapping import dict_to_string + +# TODO: we probably need to write and populate those logs as part of the handler chain itself +# and store it in the InvocationContext. That way, we could also retrieve in when calling TestInvoke + +TEST_INVOKE_TEMPLATE = """Execution log for request {request_id} +{formatted_date} : Starting execution for request: {request_id} +{formatted_date} : HTTP Method: {request_method}, Resource Path: {resource_path} +{formatted_date} : Method request path: {method_request_path_parameters} +{formatted_date} : Method request query string: {method_request_query_string} +{formatted_date} : Method request headers: {method_request_headers} +{formatted_date} : Method request body before transformations: {method_request_body} +{formatted_date} : Endpoint request URI: {endpoint_uri} +{formatted_date} : Endpoint request headers: {endpoint_request_headers} +{formatted_date} : Endpoint request body after transformations: {endpoint_request_body} +{formatted_date} : Sending request to {endpoint_uri} +{formatted_date} : Received response. Status: {endpoint_response_status_code}, Integration latency: {endpoint_response_latency} ms +{formatted_date} : Endpoint response headers: {endpoint_response_headers} +{formatted_date} : Endpoint response body before transformations: {endpoint_response_body} +{formatted_date} : Method response body after transformations: {method_response_body} +{formatted_date} : Method response headers: {method_response_headers} +{formatted_date} : Successfully completed execution +{formatted_date} : Method completed with status: {method_response_status} +""" + + +def _dump_headers(headers: Headers) -> str: + if not headers: + return "{}" + multi_headers = {key: ",".join(headers.getlist(key)) for key in headers.keys()} + string_headers = dict_to_string(multi_headers) + if len(string_headers) > 998: + return f"{string_headers[:998]} [TRUNCATED]" + + return string_headers + + +def log_template(invocation_context: RestApiInvocationContext, response_headers: Headers) -> str: + # TODO: funny enough, in AWS for the `endpoint_response_headers` in AWS_PROXY, they log the response headers from + # lambda HTTP Invoke call even though we use the headers from the lambda response itself + formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y") + request = invocation_context.invocation_request + context_var = invocation_context.context_variables + integration_req = invocation_context.integration_request + endpoint_resp = invocation_context.endpoint_response + method_resp = invocation_context.invocation_response + # TODO: if endpoint_uri is an ARN, it means it's an AWS_PROXY integration + # this should be transformed to the true URL of a lambda invoke call + endpoint_uri = integration_req.get("uri", "") + + return TEST_INVOKE_TEMPLATE.format( + formatted_date=formatted_date, + request_id=context_var["requestId"], + resource_path=request["path"], + request_method=request["http_method"], + method_request_path_parameters=dict_to_string(request["path_parameters"]), + method_request_query_string=dict_to_string(request["query_string_parameters"]), + method_request_headers=_dump_headers(request.get("headers")), + method_request_body=to_str(request.get("body", "")), + endpoint_uri=endpoint_uri, + endpoint_request_headers=_dump_headers(integration_req.get("headers")), + endpoint_request_body=to_str(integration_req.get("body", "")), + # TODO: measure integration latency + endpoint_response_latency=150, + endpoint_response_status_code=endpoint_resp.get("status_code"), + endpoint_response_body=to_str(endpoint_resp.get("body", "")), + endpoint_response_headers=_dump_headers(endpoint_resp.get("headers")), + method_response_status=method_resp.get("status_code"), + method_response_body=to_str(method_resp.get("body", "")), + method_response_headers=_dump_headers(response_headers), + ) + + +def create_test_chain() -> HandlerChain[RestApiInvocationContext]: + return HandlerChain( + request_handlers=[ + handlers.method_request_handler, + handlers.integration_request_handler, + handlers.integration_handler, + handlers.integration_response_handler, + handlers.method_response_handler, + ], + exception_handlers=[ + handlers.gateway_exception_handler, + ], + ) + + +def create_test_invocation_context( + test_request: TestInvokeMethodRequest, + deployment: RestApiDeployment, +) -> RestApiInvocationContext: + parse_handler = handlers.parse_request + http_method = test_request["httpMethod"] + + # we do not need a true HTTP request for the context, as we are skipping all the parsing steps and using the + # provider data + invocation_context = RestApiInvocationContext( + request=Request(method=http_method), + ) + path_query = test_request.get("pathWithQueryString", "/").split("?") + path = path_query[0] + multi_query_args: dict[str, list[str]] = {} + + if len(path_query) > 1: + multi_query_args = parse_qs(path_query[1]) + + # for the single value parameters, AWS only keeps the last value of the list + single_query_args = {k: v[-1] for k, v in multi_query_args.items()} + + invocation_request = InvocationRequest( + http_method=http_method, + path=path, + raw_path=path, + query_string_parameters=single_query_args, + multi_value_query_string_parameters=multi_query_args, + headers=Headers(test_request.get("headers")), + # TODO: handle multiValueHeaders + body=to_bytes(test_request.get("body") or ""), + ) + invocation_context.invocation_request = invocation_request + + _, path_parameters = RestAPIResourceRouter(deployment).match(invocation_context) + invocation_request["path_parameters"] = path_parameters + + invocation_context.deployment = deployment + invocation_context.api_id = test_request["restApiId"] + invocation_context.stage = None + invocation_context.deployment_id = "" + invocation_context.account_id = deployment.account_id + invocation_context.region = deployment.region + invocation_context.stage_variables = test_request.get("stageVariables", {}) + invocation_context.context_variables = parse_handler.create_context_variables( + invocation_context + ) + invocation_context.trace_id = parse_handler.populate_trace_id({}) + + resource = deployment.rest_api.resources[test_request["resourceId"]] + resource_method = resource["resourceMethods"][http_method] + invocation_context.resource = resource + invocation_context.resource_method = resource_method + invocation_context.integration = resource_method["methodIntegration"] + handlers.route_request.update_context_variables_with_resource( + invocation_context.context_variables, resource + ) + + return invocation_context + + +def run_test_invocation( + test_request: TestInvokeMethodRequest, deployment: RestApiDeployment +) -> TestInvokeMethodResponse: + # validate resource exists in deployment + invocation_context = create_test_invocation_context(test_request, deployment) + + test_chain = create_test_chain() + # header order is important + if invocation_context.integration["type"] == "MOCK": + base_headers = {"Content-Type": APPLICATION_JSON} + else: + # we manually add the trace-id, as it is normally added by handlers.response_enricher which adds to much data + # for the TestInvoke. It needs to be first + base_headers = { + "X-Amzn-Trace-Id": invocation_context.trace_id, + "Content-Type": APPLICATION_JSON, + } + + test_response = Response(headers=base_headers) + start_time = datetime.datetime.now() + test_chain.handle(context=invocation_context, response=test_response) + end_time = datetime.datetime.now() + + response_headers = test_response.headers.copy() + # AWS does not return the Content-Length for TestInvokeMethod + response_headers.remove("Content-Length") + + log = log_template(invocation_context, response_headers) + + headers = dict(response_headers) + multi_value_headers = build_multi_value_headers(response_headers) + + return TestInvokeMethodResponse( + log=log, + status=test_response.status_code, + body=test_response.get_data(as_text=True), + headers=headers, + multiValueHeaders=multi_value_headers, + latency=int((end_time - start_time).total_seconds()), + ) diff --git a/localstack-core/localstack/services/apigateway/next_gen/provider.py b/localstack-core/localstack/services/apigateway/next_gen/provider.py index 9361e08ae94fd..9c3dab33bfe86 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/provider.py +++ b/localstack-core/localstack/services/apigateway/next_gen/provider.py @@ -37,6 +37,7 @@ ) from .execute_api.helpers import freeze_rest_api from .execute_api.router import ApiGatewayEndpoint, ApiGatewayRouter +from .execute_api.test_invoke import run_test_invocation class ApigatewayNextGenProvider(ApigatewayProvider): @@ -242,8 +243,28 @@ def get_gateway_responses( def test_invoke_method( self, context: RequestContext, request: TestInvokeMethodRequest ) -> TestInvokeMethodResponse: - # TODO: rewrite and migrate to NextGen - return super().test_invoke_method(context, request) + rest_api_id = request["restApiId"] + moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id) + resource = moto_rest_api.resources.get(request["resourceId"]) + if not resource: + raise NotFoundException("Invalid Resource identifier specified") + + # test httpMethod + + rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id) + frozen_deployment = freeze_rest_api( + account_id=context.account_id, + region=context.region, + moto_rest_api=moto_rest_api, + localstack_rest_api=rest_api_container, + ) + + response = run_test_invocation( + test_request=request, + deployment=frozen_deployment, + ) + + return response def _get_gateway_response_or_default( diff --git a/tests/aws/services/apigateway/apigateway_fixtures.py b/tests/aws/services/apigateway/apigateway_fixtures.py index 0c0b549032df0..e7d58b40c5ba2 100644 --- a/tests/aws/services/apigateway/apigateway_fixtures.py +++ b/tests/aws/services/apigateway/apigateway_fixtures.py @@ -35,74 +35,24 @@ def import_rest_api(apigateway_client, **kwargs): return response, root_id -def get_rest_api(apigateway_client, **kwargs): - response = apigateway_client.get_rest_api(**kwargs) - assert_response_is_200(response) - return response.get("id"), response.get("name") - - -def put_rest_api(apigateway_client, **kwargs): - response = apigateway_client.put_rest_api(**kwargs) - assert_response_is_200(response) - return response.get("id"), response.get("name") - - -def get_rest_apis(apigateway_client, **kwargs): - response = apigateway_client.get_rest_apis(**kwargs) - assert_response_is_200(response) - return response.get("items") - - -def delete_rest_api(apigateway_client, **kwargs): - response = apigateway_client.delete_rest_api(**kwargs) - assert_response_status(response, 202) - - def create_rest_resource(apigateway_client, **kwargs): response = apigateway_client.create_resource(**kwargs) assert_response_is_201(response) return response.get("id"), response.get("parentId") -def delete_rest_resource(apigateway_client, **kwargs): - response = apigateway_client.delete_resource(**kwargs) - assert_response_is_200(response) - - def create_rest_resource_method(apigateway_client, **kwargs): response = apigateway_client.put_method(**kwargs) assert_response_is_201(response) return response.get("httpMethod"), response.get("authorizerId") -def create_rest_authorizer(apigateway_client, **kwargs): - response = apigateway_client.create_authorizer(**kwargs) - assert_response_is_201(response) - return response.get("id"), response.get("type") - - def create_rest_api_integration(apigateway_client, **kwargs): response = apigateway_client.put_integration(**kwargs) assert_response_is_201(response) return response.get("uri"), response.get("type") -def get_rest_api_resources(apigateway_client, **kwargs): - response = apigateway_client.get_resources(**kwargs) - assert_response_is_200(response) - return response.get("items") - - -def delete_rest_api_integration(apigateway_client, **kwargs): - response = apigateway_client.delete_integration(**kwargs) - assert_response_is_200(response) - - -def get_rest_api_integration(apigateway_client, **kwargs): - response = apigateway_client.get_integration(**kwargs) - assert_response_is_200(response) - - def create_rest_api_method_response(apigateway_client, **kwargs): response = apigateway_client.put_method_response(**kwargs) assert_response_is_201(response) @@ -115,17 +65,6 @@ def create_rest_api_integration_response(apigateway_client, **kwargs): return response.get("statusCode") -def create_domain_name(apigateway_client, **kwargs): - response = apigateway_client.create_domain_name(**kwargs) - assert_response_is_201(response) - - -def create_base_path_mapping(apigateway_client, **kwargs): - response = apigateway_client.create_base_path_mapping(**kwargs) - assert_response_is_201(response) - return response.get("basePath"), response.get("stage") - - def create_rest_api_deployment(apigateway_client, **kwargs): response = apigateway_client.create_deployment(**kwargs) assert_response_is_201(response) @@ -150,47 +89,6 @@ def update_rest_api_stage(apigateway_client, **kwargs): return response.get("stageName") -def create_cognito_user_pool(cognito_idp, **kwargs): - response = cognito_idp.create_user_pool(**kwargs) - assert_response_is_200(response) - return response.get("UserPool").get("Id"), response.get("UserPool").get("Arn") - - -def delete_cognito_user_pool(cognito_idp, **kwargs): - response = cognito_idp.delete_user_pool(**kwargs) - assert_response_is_200(response) - - -def create_cognito_user_pool_client(cognito_idp, **kwargs): - response = cognito_idp.create_user_pool_client(**kwargs) - assert_response_is_200(response) - return ( - response.get("UserPoolClient").get("ClientId"), - response.get("UserPoolClient").get("ClientName"), - ) - - -def create_cognito_user(cognito_idp, **kwargs): - response = cognito_idp.sign_up(**kwargs) - assert_response_is_200(response) - - -def create_cognito_sign_up_confirmation(cognito_idp, **kwargs): - response = cognito_idp.admin_confirm_sign_up(**kwargs) - assert_response_is_200(response) - - -def create_initiate_auth(cognito_idp, **kwargs): - response = cognito_idp.initiate_auth(**kwargs) - assert_response_is_200(response) - return response.get("AuthenticationResult").get("IdToken") - - -def delete_cognito_user_pool_client(cognito_idp, **kwargs): - response = cognito_idp.delete_user_pool_client(**kwargs) - assert_response_is_200(response) - - # # Common utilities # diff --git a/tests/aws/services/apigateway/conftest.py b/tests/aws/services/apigateway/conftest.py index d593e084496d7..88ac5575de221 100644 --- a/tests/aws/services/apigateway/conftest.py +++ b/tests/aws/services/apigateway/conftest.py @@ -13,7 +13,6 @@ create_rest_api_stage, create_rest_resource, create_rest_resource_method, - delete_rest_api, import_rest_api, ) from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE @@ -232,7 +231,7 @@ def _import_apigateway_function(*args, **kwargs): yield _import_apigateway_function for rest_api_id in rest_api_ids: - delete_rest_api(apigateway_client, restApiId=rest_api_id) + apigateway_client.delete_rest_api(restApiId=rest_api_id) @pytest.fixture diff --git a/tests/aws/services/apigateway/test_apigateway_api.py b/tests/aws/services/apigateway/test_apigateway_api.py index 847fca937bcc4..686df0ba88a85 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.py +++ b/tests/aws/services/apigateway/test_apigateway_api.py @@ -2320,6 +2320,7 @@ def test_invoke_test_method(self, create_rest_apigw, snapshot, aws_client): lambda k, v: str(v) if k == "latency" else None, "latency", replace_reference=False ) ) + # TODO: maybe transformer `log` better snapshot.add_transformer( snapshot.transform.key_value("log", "log", reference_replacement=False) ) diff --git a/tests/aws/services/apigateway/test_apigateway_basic.py b/tests/aws/services/apigateway/test_apigateway_basic.py index ef984d8c99975..949e22cacbcd0 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.py +++ b/tests/aws/services/apigateway/test_apigateway_basic.py @@ -54,8 +54,6 @@ create_rest_api_stage, create_rest_resource, create_rest_resource_method, - delete_rest_api, - get_rest_api, update_rest_api_deployment, update_rest_api_stage, ) @@ -149,9 +147,8 @@ def test_create_rest_api_with_custom_id(self, create_rest_apigw, url_function, a api_id, name, _ = create_rest_apigw(name=apigw_name, tags={TAG_KEY_CUSTOM_ID: test_id}) assert test_id == api_id assert apigw_name == name - api_id, name = get_rest_api(aws_client.apigateway, restApiId=test_id) - assert test_id == api_id - assert apigw_name == name + response = aws_client.apigateway.get_rest_api(restApiId=test_id) + assert response["name"] == apigw_name spec_file = load_file(TEST_IMPORT_MOCK_INTEGRATION) aws_client.apigateway.put_rest_api(restApiId=test_id, body=spec_file, mode="overwrite") @@ -1207,6 +1204,20 @@ def invoke_api(): @markers.aws.validated @markers.snapshot.skip_snapshot_verify( + paths=[ + # the Endpoint URI is wrong for AWS_PROXY because AWS resolves it to the Lambda HTTP endpoint and we keep + # the ARN + "$..log.line07", + "$..log.line10", + # AWS is returning the AWS_PROXY invoke response headers even though they are not considered at all (only + # the lambda payload headers are considered, so this is unhelpful) + "$..log.line12", + # LocalStack does not setup headers the same way when invoking the lambda (Token, additional headers...) + "$..log.line08", + ] + ) + @markers.snapshot.skip_snapshot_verify( + condition=lambda: not is_next_gen_api(), paths=[ "$..headers.Content-Length", "$..headers.Content-Type", @@ -1216,7 +1227,7 @@ def invoke_api(): "$..multiValueHeaders.Content-Length", "$..multiValueHeaders.Content-Type", "$..multiValueHeaders.X-Amzn-Trace-Id", - ] + ], ) def test_apigw_test_invoke_method_api( self, @@ -1227,6 +1238,41 @@ def test_apigw_test_invoke_method_api( region_name, snapshot, ): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value( + "latency", value_replacement="", reference_replacement=False + ), + snapshot.transform.jsonpath( + "$..headers.X-Amzn-Trace-Id", value_replacement="x-amz-trace-id" + ), + snapshot.transform.regex( + r"URI: https:\/\/.*?\/2015-03-31", "URI: https:///2015-03-31" + ), + snapshot.transform.regex( + r"Integration latency: \d*? ms", "Integration latency: ms" + ), + snapshot.transform.regex( + r"Date=[a-zA-Z]{3},\s\d{2}\s[a-zA-Z]{3}\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT", + "Date=Day, dd MMM yyyy hh:mm:ss GMT", + ), + snapshot.transform.regex( + r"x-amzn-RequestId=[a-f0-9-]{36}", "x-amzn-RequestId=" + ), + snapshot.transform.regex( + r"[a-zA-Z]{3}\s[a-zA-Z]{3}\s\d{2}\s\d{2}:\d{2}:\d{2}\sUTC\s\d{4} :", + "DDD MMM dd hh:mm:ss UTC yyyy :", + ), + snapshot.transform.regex( + r"Authorization=.*?,", "Authorization=," + ), + snapshot.transform.regex( + r"X-Amz-Security-Token=.*?\s\[", "X-Amz-Security-Token= [" + ), + snapshot.transform.regex(r"\d{8}T\d{6}Z", ""), + ] + ) + _, role_arn = create_role_with_policy( "Allow", "lambda:InvokeFunction", json.dumps(APIGATEWAY_ASSUME_ROLE_POLICY), "*" ) @@ -1238,14 +1284,17 @@ def test_apigw_test_invoke_method_api( handler="lambda_handler.handler", runtime=Runtime.nodejs18_x, ) + snapshot.add_transformer(snapshot.transform.regex(function_name, "")) lambda_arn = create_function_response["CreateFunctionResponse"]["FunctionArn"] target_uri = arns.apigateway_invocations_arn(lambda_arn, region_name) # create REST API and test resource rest_api_id, _, root = create_rest_apigw(name=f"test-{short_uid()}") - resource_id, _ = create_rest_resource( - aws_client.apigateway, restApiId=rest_api_id, parentId=root, pathPart="foo" + snapshot.add_transformer(snapshot.transform.regex(rest_api_id, "")) + resource = aws_client.apigateway.create_resource( + restApiId=rest_api_id, parentId=root, pathPart="foo" ) + resource_id = resource["id"] # create method and integration aws_client.apigateway.put_method( @@ -1263,8 +1312,7 @@ def test_apigw_test_invoke_method_api( uri=target_uri, credentials=role_arn, ) - status_code = create_rest_api_method_response( - aws_client.apigateway, + aws_client.apigateway.put_method_response( restApiId=rest_api_id, resourceId=resource_id, httpMethod="GET", @@ -1274,46 +1322,64 @@ def test_apigw_test_invoke_method_api( restApiId=rest_api_id, resourceId=resource_id, httpMethod="GET", - statusCode=status_code, + statusCode="200", selectionPattern="", ) - deployment_id, _ = create_rest_api_deployment(aws_client.apigateway, restApiId=rest_api_id) - create_rest_api_stage( - aws_client.apigateway, - restApiId=rest_api_id, - stageName="local", - deploymentId=deployment_id, - ) + aws_client.apigateway.create_deployment(restApiId=rest_api_id, stageName="local") # run test_invoke_method API #1 - def test_invoke_call(): - response = aws_client.apigateway.test_invoke_method( + def _test_invoke_call( + path_with_qs: str, body: str | None = None, headers: dict | None = None + ): + kwargs = {} + if body: + kwargs["body"] = body + if headers: + kwargs["headers"] = headers + _response = aws_client.apigateway.test_invoke_method( restApiId=rest_api_id, resourceId=resource_id, httpMethod="GET", - pathWithQueryString="/foo", + pathWithQueryString=path_with_qs, + **kwargs, ) - assert 200 == response["ResponseMetadata"]["HTTPStatusCode"] - assert 200 == response.get("status") - assert "response from" in json.loads(response.get("body")).get("body") - snapshot.match("test_invoke_method_response", response) + assert _response.get("status") == 200 + assert "response from" in json.loads(_response.get("body")).get("body") + return _response + + invoke_simple = retry(_test_invoke_call, retries=15, sleep=1, path_with_qs="/foo") - retry(test_invoke_call, retries=15, sleep=1) + def _transform_log(_log: str) -> dict[str, str]: + return {f"line{index:02d}": line for index, line in enumerate(_log.split("\n"))} + + # we want to do very precise matching on the log, and splitting on new lines will help in case the snapshot + # fails + # the snapshot library does not allow to ignore an array index as the last node, so we need to put it in a dict + invoke_simple["log"] = _transform_log(invoke_simple["log"]) + request_id_1 = invoke_simple["log"]["line00"].split(" ")[-1] + snapshot.add_transformer( + snapshot.transform.regex(request_id_1, ""), priority=-1 + ) + snapshot.match("test_invoke_method_response", invoke_simple) # run test_invoke_method API #2 - response = aws_client.apigateway.test_invoke_method( - restApiId=rest_api_id, - resourceId=resource_id, - httpMethod="GET", - pathWithQueryString="/foo", + invoke_with_parameters = retry( + _test_invoke_call, + retries=15, + sleep=1, + path_with_qs="/foo?queryTest=value", body='{"test": "val123"}', headers={"content-type": "application/json"}, ) - assert 200 == response["ResponseMetadata"]["HTTPStatusCode"] - assert 200 == response.get("status") - assert "response from" in json.loads(response.get("body")).get("body") - assert "val123" in json.loads(response.get("body")).get("body") - snapshot.match("test_invoke_method_response_with_body", response) + response_body = json.loads(invoke_with_parameters.get("body")).get("body") + assert "response from" in response_body + assert "val123" in response_body + invoke_with_parameters["log"] = _transform_log(invoke_with_parameters["log"]) + request_id_2 = invoke_with_parameters["log"]["line00"].split(" ")[-1] + snapshot.add_transformer( + snapshot.transform.regex(request_id_2, ""), priority=-1 + ) + snapshot.match("test_invoke_method_response_with_body", invoke_with_parameters) @markers.aws.validated @pytest.mark.parametrize("stage_name", ["local", "dev"]) @@ -1631,9 +1697,8 @@ def _invoke_url(url): api_us_id, stage=stage_name, path="/demo", region="us-west-1", url_type=url_type ) retry(_invoke_url, retries=20, sleep=2, url=endpoint) - - delete_rest_api(apigateway_client_eu, restApiId=api_eu_id) - delete_rest_api(apigateway_client_us, restApiId=api_us_id) + apigateway_client_eu.delete_rest_api(restApiId=api_eu_id) + apigateway_client_us.delete_rest_api(restApiId=api_us_id) class TestIntegrations: diff --git a/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json b/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json index 51574dc79b97c..4cdbcb8e1e311 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_test_invoke_method_api": { - "recorded-date": "04-02-2024, 18:48:24", + "recorded-date": "11-04-2025, 18:02:16", "recorded-content": { "test_invoke_method_response": { "body": { @@ -11,16 +11,36 @@ }, "headers": { "Content-Type": "application/json", - "X-Amzn-Trace-Id": "Root=1-65bfdbf7-1b5920a5a0a57e32194306b3;Parent=5c9925637b7d89fa;Sampled=0;lineage=59cc7ee1:0" + "X-Amzn-Trace-Id": "" + }, + "latency": "", + "log": { + "line00": "Execution log for request ", + "line01": "DDD MMM dd hh:mm:ss UTC yyyy : Starting execution for request: ", + "line02": "DDD MMM dd hh:mm:ss UTC yyyy : HTTP Method: GET, Resource Path: /foo", + "line03": "DDD MMM dd hh:mm:ss UTC yyyy : Method request path: {}", + "line04": "DDD MMM dd hh:mm:ss UTC yyyy : Method request query string: {}", + "line05": "DDD MMM dd hh:mm:ss UTC yyyy : Method request headers: {}", + "line06": "DDD MMM dd hh:mm:ss UTC yyyy : Method request body before transformations: ", + "line07": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request URI: https:///2015-03-31/functions/arn::lambda::111111111111:function:/invocations", + "line08": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request headers: {x-amzn-lambda-integration-tag=, Authorization=, X-Amz-Date=, x-amzn-apigateway-api-id=, Accept=application/json, User-Agent=AmazonAPIGateway_, X-Amz-Security-Token= [TRUNCATED]", + "line09": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request body after transformations: ", + "line10": "DDD MMM dd hh:mm:ss UTC yyyy : Sending request to https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:/invocations", + "line11": "DDD MMM dd hh:mm:ss UTC yyyy : Received response. Status: 200, Integration latency: ms", + "line12": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint response headers: {Date=Day, dd MMM yyyy hh:mm:ss GMT, Content-Type=application/json, Content-Length=104, Connection=keep-alive, x-amzn-RequestId=, x-amzn-Remapped-Content-Length=0, X-Amz-Executed-Version=$LATEST, X-Amzn-Trace-Id=}", + "line13": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint response body before transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}", + "line14": "DDD MMM dd hh:mm:ss UTC yyyy : Method response body after transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}", + "line15": "DDD MMM dd hh:mm:ss UTC yyyy : Method response headers: {X-Amzn-Trace-Id=, Content-Type=application/json}", + "line16": "DDD MMM dd hh:mm:ss UTC yyyy : Successfully completed execution", + "line17": "DDD MMM dd hh:mm:ss UTC yyyy : Method completed with status: 200", + "line18": "" }, - "latency": 394, - "log": "Execution log for request d09d726b-32a3-42fc-87c7-42ac58bca845\nSun Feb 04 18:48:23 UTC 2024 : Starting execution for request: d09d726b-32a3-42fc-87c7-42ac58bca845\nSun Feb 04 18:48:23 UTC 2024 : HTTP Method: GET, Resource Path: /foo\nSun Feb 04 18:48:23 UTC 2024 : Method request path: {}\nSun Feb 04 18:48:23 UTC 2024 : Method request query string: {}\nSun Feb 04 18:48:23 UTC 2024 : Method request headers: {}\nSun Feb 04 18:48:23 UTC 2024 : Method request body before transformations: \nSun Feb 04 18:48:23 UTC 2024 : Endpoint request URI: https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:test-de2a8789/invocations\nSun Feb 04 18:48:23 UTC 2024 : Endpoint request headers: {x-amzn-lambda-integration-tag=d09d726b-32a3-42fc-87c7-42ac58bca845, Authorization=*********************************************************************************************************************************************************************************************************************************************************************fd20ad, X-Amz-Date=20240204T184823Z, x-amzn-apigateway-api-id=96m844vit9, Accept=application/json, User-Agent=AmazonAPIGateway_96m844vit9, X-Amz-Security-Token=IQoJb3JpZ2luX2VjEMv//////////wEaCXVzLWVhc3QtMSJHMEUCIQDH/nm1y4gMfoEBmxGW3/Tvqy4n6O3lzViNg021ao2NOQIgXFf6aGDn2L5egYErKkRsBaOKEvTn/jpaZgmTjAGO1BEq7gIIlP//////////ARACGgw2NTk2NzY4MjExMTgiDGZzbbOVj3R7zPeswyrCAtEzQYGuVCS1ylMX93oVtpfyXNQx3ZLeknme7FtyuuFFuzM2lU+a3C4ykL4j8qQmT8nFXdfX7ZzLCLmRjr1EhTgPrh7SE5XSxfBQdxTQxkoaGImnDRbceKLPxSMALrub+owhkfeZT29laOyBzPdttLM7iG7Q/bws/ywC0I8HMJA4Dl5KHMhiKDBncYXjdYhlHCSPb+qN/5cZ1Wm+jUV/znw6RG8Hhz+mKzFDckbVItiRD+CdbP5V3IjVZgtzSvwXqN8EXN9R0tRXE+b0FD7AUMctWoDbCqkIHf [TRUNCATED]\nSun Feb 04 18:48:23 UTC 2024 : Endpoint request body after transformations: \nSun Feb 04 18:48:23 UTC 2024 : Sending request to https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:test-de2a8789/invocations\nSun Feb 04 18:48:24 UTC 2024 : Received response. Status: 200, Integration latency: 356 ms\nSun Feb 04 18:48:24 UTC 2024 : Endpoint response headers: {Date=Sun, 04 Feb 2024 18:48:24 GMT, Content-Type=application/json, Content-Length=104, Connection=keep-alive, x-amzn-RequestId=20a0cc6d-ade0-417f-853d-04c72dbe23d6, x-amzn-Remapped-Content-Length=0, X-Amz-Executed-Version=$LATEST, X-Amzn-Trace-Id=root=1-65bfdbf7-1b5920a5a0a57e32194306b3;parent=5c9925637b7d89fa;sampled=0;lineage=59cc7ee1:0}\nSun Feb 04 18:48:24 UTC 2024 : Endpoint response body before transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}\nSun Feb 04 18:48:24 UTC 2024 : Method response body after transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}\nSun Feb 04 18:48:24 UTC 2024 : Method response headers: {X-Amzn-Trace-Id=Root=1-65bfdbf7-1b5920a5a0a57e32194306b3;Parent=5c9925637b7d89fa;Sampled=0;lineage=59cc7ee1:0, Content-Type=application/json}\nSun Feb 04 18:48:24 UTC 2024 : Successfully completed execution\nSun Feb 04 18:48:24 UTC 2024 : Method completed with status: 200\n", "multiValueHeaders": { "Content-Type": [ "application/json" ], "X-Amzn-Trace-Id": [ - "Root=1-65bfdbf7-1b5920a5a0a57e32194306b3;Parent=5c9925637b7d89fa;Sampled=0;lineage=59cc7ee1:0" + "" ] }, "status": 200, @@ -38,16 +58,36 @@ }, "headers": { "Content-Type": "application/json", - "X-Amzn-Trace-Id": "Root=1-65bfdbf8-caa70673935f456b40debcda;Parent=0f5819866f6639ce;Sampled=0;lineage=59cc7ee1:0" + "X-Amzn-Trace-Id": "" + }, + "latency": "", + "log": { + "line00": "Execution log for request ", + "line01": "DDD MMM dd hh:mm:ss UTC yyyy : Starting execution for request: ", + "line02": "DDD MMM dd hh:mm:ss UTC yyyy : HTTP Method: GET, Resource Path: /foo", + "line03": "DDD MMM dd hh:mm:ss UTC yyyy : Method request path: {}", + "line04": "DDD MMM dd hh:mm:ss UTC yyyy : Method request query string: {queryTest=value}", + "line05": "DDD MMM dd hh:mm:ss UTC yyyy : Method request headers: {content-type=application/json}", + "line06": "DDD MMM dd hh:mm:ss UTC yyyy : Method request body before transformations: {\"test\": \"val123\"}", + "line07": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request URI: https:///2015-03-31/functions/arn::lambda::111111111111:function:/invocations", + "line08": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request headers: {x-amzn-lambda-integration-tag=, Authorization=, X-Amz-Date=, x-amzn-apigateway-api-id=, Accept=application/json, User-Agent=AmazonAPIGateway_, X-Amz-Security-Token= [TRUNCATED]", + "line09": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint request body after transformations: {\"test\": \"val123\"}", + "line10": "DDD MMM dd hh:mm:ss UTC yyyy : Sending request to https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:/invocations", + "line11": "DDD MMM dd hh:mm:ss UTC yyyy : Received response. Status: 200, Integration latency: ms", + "line12": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint response headers: {Date=Day, dd MMM yyyy hh:mm:ss GMT, Content-Type=application/json, Content-Length=131, Connection=keep-alive, x-amzn-RequestId=, x-amzn-Remapped-Content-Length=0, X-Amz-Executed-Version=$LATEST, X-Amzn-Trace-Id=}", + "line13": "DDD MMM dd hh:mm:ss UTC yyyy : Endpoint response body before transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {\\\\\\\"test\\\\\\\":\\\\\\\"val123\\\\\\\"}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}", + "line14": "DDD MMM dd hh:mm:ss UTC yyyy : Method response body after transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {\\\\\\\"test\\\\\\\":\\\\\\\"val123\\\\\\\"}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}", + "line15": "DDD MMM dd hh:mm:ss UTC yyyy : Method response headers: {X-Amzn-Trace-Id=, Content-Type=application/json}", + "line16": "DDD MMM dd hh:mm:ss UTC yyyy : Successfully completed execution", + "line17": "DDD MMM dd hh:mm:ss UTC yyyy : Method completed with status: 200", + "line18": "" }, - "latency": 62, - "log": "Execution log for request 63ecf43a-1b6e-40ef-80b7-98c5b7484ec9\nSun Feb 04 18:48:24 UTC 2024 : Starting execution for request: 63ecf43a-1b6e-40ef-80b7-98c5b7484ec9\nSun Feb 04 18:48:24 UTC 2024 : HTTP Method: GET, Resource Path: /foo\nSun Feb 04 18:48:24 UTC 2024 : Method request path: {}\nSun Feb 04 18:48:24 UTC 2024 : Method request query string: {}\nSun Feb 04 18:48:24 UTC 2024 : Method request headers: {content-type=application/json}\nSun Feb 04 18:48:24 UTC 2024 : Method request body before transformations: {\"test\": \"val123\"}\nSun Feb 04 18:48:24 UTC 2024 : Endpoint request URI: https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:test-de2a8789/invocations\nSun Feb 04 18:48:24 UTC 2024 : Endpoint request headers: {x-amzn-lambda-integration-tag=63ecf43a-1b6e-40ef-80b7-98c5b7484ec9, Authorization=*******************************************************************************************************************************************************************************************************************************************************************************************************4b5ad4, X-Amz-Date=20240204T184824Z, x-amzn-apigateway-api-id=96m844vit9, Accept=application/json, User-Agent=AmazonAPIGateway_96m844vit9, X-Amz-Security-Token=IQoJb3JpZ2luX2VjEMv//////////wEaCXVzLWVhc3QtMSJIMEYCIQCX8aMq+Q5P6zw4SzP7nSzzMTzd2D0tbCwx9jyQnWiiSgIhAKevG8f4Qo1O/lr+A17AujqFg9AqJCIB5zNu+g8RZFl+Ku4CCJT//////////wEQAhoMNjU5Njc2ODIxMTE4IgxyHR1NVV6IvXrBrD8qwgJNyGLqGkyhoWFD36VE4ENpEW9PzKtbnKkQq/tqZdBBSwvzTmANSNEE7dIpiTolgXGMN4llNaV9CNYF+Ro/zXmsY4u/y8HgSFnTst/iOam+hEGQEr9BEflhu1Sqy7xqBt5pfIVscdpPNVsdX0OLKDT98v3pTRUnilsMDK/6F4wzl4SJ8mQ4vYqCN5mh6n+96Ze2Q0ldYEDjbBmMItgyDk2so2OxMiVPtrhJ81u7NYsEYdmgQ5dve3rQYT7+oVnA [TRUNCATED]\nSun Feb 04 18:48:24 UTC 2024 : Endpoint request body after transformations: {\"test\": \"val123\"}\nSun Feb 04 18:48:24 UTC 2024 : Sending request to https://lambda..amazonaws.com/2015-03-31/functions/arn::lambda::111111111111:function:test-de2a8789/invocations\nSun Feb 04 18:48:24 UTC 2024 : Received response. Status: 200, Integration latency: 25 ms\nSun Feb 04 18:48:24 UTC 2024 : Endpoint response headers: {Date=Sun, 04 Feb 2024 18:48:24 GMT, Content-Type=application/json, Content-Length=131, Connection=keep-alive, x-amzn-RequestId=57dc53e3-bc2e-449b-83ef-fd7d97479909, x-amzn-Remapped-Content-Length=0, X-Amz-Executed-Version=$LATEST, X-Amzn-Trace-Id=root=1-65bfdbf8-caa70673935f456b40debcda;parent=0f5819866f6639ce;sampled=0;lineage=59cc7ee1:0}\nSun Feb 04 18:48:24 UTC 2024 : Endpoint response body before transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {\\\\\\\"test\\\\\\\":\\\\\\\"val123\\\\\\\"}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}\nSun Feb 04 18:48:24 UTC 2024 : Method response body after transformations: {\"statusCode\":200,\"body\":\"\\\"response from localstack lambda: {\\\\\\\"test\\\\\\\":\\\\\\\"val123\\\\\\\"}\\\"\",\"isBase64Encoded\":false,\"headers\":{}}\nSun Feb 04 18:48:24 UTC 2024 : Method response headers: {X-Amzn-Trace-Id=Root=1-65bfdbf8-caa70673935f456b40debcda;Parent=0f5819866f6639ce;Sampled=0;lineage=59cc7ee1:0, Content-Type=application/json}\nSun Feb 04 18:48:24 UTC 2024 : Successfully completed execution\nSun Feb 04 18:48:24 UTC 2024 : Method completed with status: 200\n", "multiValueHeaders": { "Content-Type": [ "application/json" ], "X-Amzn-Trace-Id": [ - "Root=1-65bfdbf8-caa70673935f456b40debcda;Parent=0f5819866f6639ce;Sampled=0;lineage=59cc7ee1:0" + "" ] }, "status": 200, diff --git a/tests/aws/services/apigateway/test_apigateway_basic.validation.json b/tests/aws/services/apigateway/test_apigateway_basic.validation.json index cbb19a133ecf2..43de03144651a 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_basic.validation.json @@ -15,7 +15,7 @@ "last_validated_date": "2024-07-12T20:04:15+00:00" }, "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_test_invoke_method_api": { - "last_validated_date": "2024-02-04T18:48:24+00:00" + "last_validated_date": "2025-04-11T18:03:13+00:00" }, "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_update_rest_api_deployment": { "last_validated_date": "2024-04-12T21:24:49+00:00" From 2c9dff513f7bcecc9c27843204d83f47cab539a2 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Mon, 14 Apr 2025 01:57:33 +0200 Subject: [PATCH 049/108] add AVP to list of CFN composite quirks (#12517) --- .../localstack/services/cloudformation/engine/quirks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/localstack-core/localstack/services/cloudformation/engine/quirks.py b/localstack-core/localstack/services/cloudformation/engine/quirks.py index b38056474b560..964d5b603d960 100644 --- a/localstack-core/localstack/services/cloudformation/engine/quirks.py +++ b/localstack-core/localstack/services/cloudformation/engine/quirks.py @@ -30,6 +30,9 @@ "AWS::Logs::SubscriptionFilter": "/properties/LogGroupName", "AWS::RDS::DBProxyTargetGroup": "/properties/TargetGroupName", "AWS::Glue::SchemaVersionMetadata": "||", # composite + "AWS::VerifiedPermissions::IdentitySource": "|", # composite + "AWS::VerifiedPermissions::Policy": "|", # composite + "AWS::VerifiedPermissions::PolicyTemplate": "|", # composite "AWS::WAFv2::WebACL": "||", "AWS::WAFv2::WebACLAssociation": "|", "AWS::WAFv2::IPSet": "||", From 172445121743631c017638f7b360d75083b2becf Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Mon, 14 Apr 2025 11:13:37 +0200 Subject: [PATCH 050/108] Support IAM transitive session tagging (#12508) --- .../localstack/services/sts/models.py | 14 +- .../localstack/services/sts/provider.py | 59 +++++-- tests/aws/services/sts/test_sts.py | 149 ++++++++++++++++++ tests/aws/services/sts/test_sts.snapshot.json | 138 ++++++++++++++++ .../aws/services/sts/test_sts.validation.json | 15 ++ 5 files changed, 363 insertions(+), 12 deletions(-) diff --git a/localstack-core/localstack/services/sts/models.py b/localstack-core/localstack/services/sts/models.py index 7d4d6020b0467..67a8665dbb76f 100644 --- a/localstack-core/localstack/services/sts/models.py +++ b/localstack-core/localstack/services/sts/models.py @@ -1,9 +1,19 @@ +from typing import TypedDict + +from localstack.aws.api.sts import Tag from localstack.services.stores import AccountRegionBundle, BaseStore, CrossRegionAttribute +class SessionTaggingConfig(TypedDict): + # => {"Key": , "Value": } + tags: dict[str, Tag] + # list of lowercase transitive tag keys + transitive_tags: list[str] + + class STSStore(BaseStore): - # maps access key ids to tags for the session they belong to - session_tags: dict[str, dict[str, str]] = CrossRegionAttribute(default=dict) + # maps access key ids to tagging config for the session they belong to + session_tags: dict[str, SessionTaggingConfig] = CrossRegionAttribute(default=dict) sts_stores = AccountRegionBundle("sts", STSStore) diff --git a/localstack-core/localstack/services/sts/provider.py b/localstack-core/localstack/services/sts/provider.py index 006a510a612ce..14807869ea9cb 100644 --- a/localstack-core/localstack/services/sts/provider.py +++ b/localstack-core/localstack/services/sts/provider.py @@ -1,6 +1,6 @@ import logging -from localstack.aws.api import RequestContext +from localstack.aws.api import RequestContext, ServiceException from localstack.aws.api.sts import ( AssumeRoleResponse, GetCallerIdentityResponse, @@ -21,12 +21,19 @@ from localstack.services.iam.iam_patches import apply_iam_patches from localstack.services.moto import call_moto from localstack.services.plugins import ServiceLifecycleHook -from localstack.services.sts.models import sts_stores +from localstack.services.sts.models import SessionTaggingConfig, sts_stores from localstack.utils.aws.arns import extract_account_id_from_arn +from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header LOG = logging.getLogger(__name__) +class InvalidParameterValueError(ServiceException): + code = "InvalidParameterValue" + status_code = 400 + sender_fault = True + + class StsProvider(StsApi, ServiceLifecycleHook): def __init__(self): apply_iam_patches() @@ -54,15 +61,47 @@ def assume_role( provided_contexts: ProvidedContextsListType = None, **kwargs, ) -> AssumeRoleResponse: - response: AssumeRoleResponse = call_moto(context) + target_account_id = extract_account_id_from_arn(role_arn) + access_key_id = extract_access_key_id_from_auth_header(context.request.headers) + store = sts_stores[target_account_id]["us-east-1"] + existing_tagging_config = store.session_tags.get(access_key_id, {}) if tags: - transformed_tags = {tag["Key"]: tag["Value"] for tag in tags} - # we should save it in the store of the role account, not the requester - account_id = extract_account_id_from_arn(role_arn) - # the region is hardcoded to "us-east-1" as IAM/STS are global services - # this will only differ for other partitions, which are not yet supported - store = sts_stores[account_id]["us-east-1"] + tag_keys = {tag["Key"].lower() for tag in tags} + # if the lower-cased set is smaller than the number of keys, there have to be some duplicates. + if len(tag_keys) < len(tags): + raise InvalidParameterValueError( + "Duplicate tag keys found. Please note that Tag keys are case insensitive." + ) + + # prevent transitive tags from being overridden + if existing_tagging_config: + if set(existing_tagging_config["transitive_tags"]).intersection(tag_keys): + raise InvalidParameterValueError( + "One of the specified transitive tag keys can't be set because it conflicts with a transitive tag key from the calling session." + ) + if transitive_tag_keys: + transitive_tag_key_set = {key.lower() for key in transitive_tag_keys} + if not transitive_tag_key_set <= tag_keys: + raise InvalidParameterValueError( + "The specified transitive tag key must be included in the requested tags." + ) + + response: AssumeRoleResponse = call_moto(context) + + transitive_tag_keys = transitive_tag_keys or [] + tags = tags or [] + transformed_tags = {tag["Key"].lower(): tag for tag in tags} + # propagate transitive tags + if existing_tagging_config: + for tag in existing_tagging_config["transitive_tags"]: + transformed_tags[tag] = existing_tagging_config["tags"][tag] + transitive_tag_keys += existing_tagging_config["transitive_tags"] + if transformed_tags: + # store session tagging config access_key_id = response["Credentials"]["AccessKeyId"] - store.session_tags[access_key_id] = transformed_tags + store.session_tags[access_key_id] = SessionTaggingConfig( + tags=transformed_tags, + transitive_tags=[key.lower() for key in transitive_tag_keys], + ) return response diff --git a/tests/aws/services/sts/test_sts.py b/tests/aws/services/sts/test_sts.py index 888e7f83a3e60..9299d88bbcba4 100644 --- a/tests/aws/services/sts/test_sts.py +++ b/tests/aws/services/sts/test_sts.py @@ -3,6 +3,7 @@ import pytest import requests +from botocore.exceptions import ClientError from localstack import config from localstack.constants import APPLICATION_JSON @@ -321,3 +322,151 @@ def test_get_caller_identity_role_access_key( response = sts_role_client_2.get_caller_identity() assert fake_account_id == response["Account"] assert assume_role_response_other_account["AssumedRoleUser"]["Arn"] == response["Arn"] + + +class TestSTSAssumeRoleTagging: + @markers.aws.validated + def test_iam_role_chaining_override_transitive_tags( + self, + aws_client, + aws_client_factory, + create_role, + snapshot, + region_name, + account_id, + wait_and_assume_role, + ): + snapshot.add_transformer(snapshot.transform.iam_api()) + role_name_1 = f"role-1-{short_uid()}" + role_name_2 = f"role-2-{short_uid()}" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sts:AssumeRole", "sts:TagSession"], + "Principal": {"AWS": account_id}, + } + ], + } + + role_1 = create_role( + RoleName=role_name_1, AssumeRolePolicyDocument=json.dumps(assume_role_policy_document) + ) + snapshot.match("role-1", role_1) + role_2 = create_role( + RoleName=role_name_2, + AssumeRolePolicyDocument=json.dumps(assume_role_policy_document), + ) + snapshot.match("role-2", role_2) + aws_client.iam.put_role_policy( + RoleName=role_name_1, + PolicyName=f"policy-{short_uid()}", + PolicyDocument=json.dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sts:AssumeRole", "sts:TagSession"], + "Resource": [role_2["Role"]["Arn"]], + } + ], + } + ), + ) + + # assume role 1 with transitive tags + keys = wait_and_assume_role( + role_arn=role_1["Role"]["Arn"], + session_name="Session1", + Tags=[{"Key": "SessionTag1", "Value": "SessionValue1"}], + TransitiveTagKeys=["SessionTag1"], + ) + role_1_clients = aws_client_factory( + aws_access_key_id=keys["AccessKeyId"], + aws_secret_access_key=keys["SecretAccessKey"], + aws_session_token=keys["SessionToken"], + ) + + # try to assume role 2 by overriding transitive session tags + with pytest.raises(ClientError) as e: + role_1_clients.sts.assume_role( + RoleArn=role_2["Role"]["Arn"], + RoleSessionName="Session2SessionTagOverride", + Tags=[{"Key": "SessionTag1", "Value": "SessionValue2"}], + ) + snapshot.match("override-transitive-tag-error", e.value.response) + + # try to assume role 2 by overriding transitive session tags but with different casing + with pytest.raises(ClientError) as e: + role_1_clients.sts.assume_role( + RoleArn=role_2["Role"]["Arn"], + RoleSessionName="Session2SessionTagOverride", + Tags=[{"Key": "sessiontag1", "Value": "SessionValue2"}], + ) + snapshot.match("override-transitive-tag-case-ignore-error", e.value.response) + + @markers.aws.validated + def test_assume_role_tag_validation( + self, + aws_client, + aws_client_factory, + create_role, + snapshot, + region_name, + account_id, + wait_and_assume_role, + ): + snapshot.add_transformer(snapshot.transform.iam_api()) + role_name_1 = f"role-1-{short_uid()}" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["sts:AssumeRole", "sts:TagSession"], + "Principal": {"AWS": account_id}, + } + ], + } + + role_1 = create_role( + RoleName=role_name_1, AssumeRolePolicyDocument=json.dumps(assume_role_policy_document) + ) + snapshot.match("role-1", role_1) + + # wait until role 1 is ready to be assumed + wait_and_assume_role( + role_arn=role_1["Role"]["Arn"], + session_name="Session1", + ) + with pytest.raises(ClientError) as e: + aws_client.sts.assume_role( + RoleArn=role_1["Role"]["Arn"], + RoleSessionName="SessionInvalidTransitiveKeys", + Tags=[{"Key": "SessionTag1", "Value": "SessionValue1"}], + TransitiveTagKeys=["InvalidKey"], + ) + snapshot.match("invalid-transitive-tag-keys", e.value.response) + + # transitive tags are case insensitive + aws_client.sts.assume_role( + RoleArn=role_1["Role"]["Arn"], + RoleSessionName="SessionInvalidCasingTransitiveKeys", + Tags=[{"Key": "SessionTag1", "Value": "SessionValue1"}], + TransitiveTagKeys=["sessiontag1"], + ) + + # identical tags with different casing in key names are invalid + with pytest.raises(ClientError) as e: + aws_client.sts.assume_role( + RoleArn=role_1["Role"]["Arn"], + RoleSessionName="SessionInvalidCasingTransitiveKeys", + Tags=[ + {"Key": "SessionTag1", "Value": "SessionValue1"}, + {"Key": "sessiontag1", "Value": "SessionValue2"}, + ], + TransitiveTagKeys=["sessiontag1"], + ) + snapshot.match("duplicate-tag-keys-different-casing", e.value.response) diff --git a/tests/aws/services/sts/test_sts.snapshot.json b/tests/aws/services/sts/test_sts.snapshot.json index 9b25e5c7ab78b..b9c07c65bc9d5 100644 --- a/tests/aws/services/sts/test_sts.snapshot.json +++ b/tests/aws/services/sts/test_sts.snapshot.json @@ -69,5 +69,143 @@ } } } + }, + "tests/aws/services/sts/test_sts.py::TestSTSAssumeRoleTagging::test_assume_role_tag_validation": { + "recorded-date": "10-04-2025, 08:53:12", + "recorded-content": { + "role-1": { + "Role": { + "Arn": "arn::iam::111111111111:role/", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ], + "Effect": "Allow", + "Principal": { + "AWS": "111111111111" + } + } + ], + "Version": "2012-10-17" + }, + "CreateDate": "", + "Path": "/", + "RoleId": "", + "RoleName": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "invalid-transitive-tag-keys": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "The specified transitive tag key must be included in the requested tags.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "duplicate-tag-keys-different-casing": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "Duplicate tag keys found. Please note that Tag keys are case insensitive.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sts/test_sts.py::TestSTSAssumeRoleTagging::test_iam_role_chaining_override_transitive_tags": { + "recorded-date": "10-04-2025, 08:53:00", + "recorded-content": { + "role-1": { + "Role": { + "Arn": "arn::iam::111111111111:role/", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ], + "Effect": "Allow", + "Principal": { + "AWS": "111111111111" + } + } + ], + "Version": "2012-10-17" + }, + "CreateDate": "", + "Path": "/", + "RoleId": "", + "RoleName": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "role-2": { + "Role": { + "Arn": "arn::iam::111111111111:role/", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ], + "Effect": "Allow", + "Principal": { + "AWS": "111111111111" + } + } + ], + "Version": "2012-10-17" + }, + "CreateDate": "", + "Path": "/", + "RoleId": "", + "RoleName": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "override-transitive-tag-error": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "One of the specified transitive tag keys can't be set because it conflicts with a transitive tag key from the calling session.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "override-transitive-tag-case-ignore-error": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "One of the specified transitive tag keys can't be set because it conflicts with a transitive tag key from the calling session.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/sts/test_sts.validation.json b/tests/aws/services/sts/test_sts.validation.json index b1e39935f0844..e651d68a58e60 100644 --- a/tests/aws/services/sts/test_sts.validation.json +++ b/tests/aws/services/sts/test_sts.validation.json @@ -1,8 +1,23 @@ { + "tests/aws/services/sts/test_sts.py::TestSTSAssumeRoleTagging::test_assume_role_tag_validation": { + "last_validated_date": "2025-04-10T08:53:12+00:00" + }, + "tests/aws/services/sts/test_sts.py::TestSTSAssumeRoleTagging::test_iam_role_chaining_override_transitive_tags": { + "last_validated_date": "2025-04-10T08:53:00+00:00" + }, "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role": { "last_validated_date": "2024-06-05T17:23:49+00:00" }, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role_invalid_tags": { + "last_validated_date": "2025-04-09T14:30:56+00:00" + }, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role_tag_validation": { + "last_validated_date": "2025-04-10T08:31:58+00:00" + }, "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_federation_token": { "last_validated_date": "2024-06-05T13:39:17+00:00" + }, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_iam_role_chaining_override_transitive_tags": { + "last_validated_date": "2025-04-10T08:08:37+00:00" } } From 5ad7f8ee8cb5864e123380f89b4c0555187c1da9 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 14 Apr 2025 16:11:10 +0200 Subject: [PATCH 051/108] feat: propagate x-ray trace id to event bridge targets (#12481) Co-authored-by: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> --- .../localstack/services/events/provider.py | 19 +- .../localstack/services/events/target.py | 74 ++- .../events/test_x_ray_trace_propagation.py | 434 ++++++++++++++++++ ...st_x_ray_trace_propagation.validation.json | 17 + tests/aws/services/lambda_/test_lambda.py | 1 + 5 files changed, 520 insertions(+), 25 deletions(-) create mode 100644 tests/aws/services/events/test_x_ray_trace_propagation.py create mode 100644 tests/aws/services/events/test_x_ray_trace_propagation.validation.json diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index cdb6e3ad32904..1a5b4ab485689 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -171,6 +171,7 @@ from localstack.utils.event_matcher import matches_event from localstack.utils.strings import long_uid from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp +from localstack.utils.xray.trace_header import TraceHeader from .analytics import InvocationStatus, rule_invocation @@ -1541,8 +1542,11 @@ def func(*args, **kwargs): } target_unique_id = f"{rule.arn}-{target['Id']}" target_sender = self._target_sender_store[target_unique_id] + new_trace_header = ( + TraceHeader().ensure_root_exists() + ) # scheduled events will always start a new trace try: - target_sender.process_event(event.copy()) + target_sender.process_event(event.copy(), trace_header=new_trace_header) except Exception as e: LOG.info( "Unable to send event notification %s to target %s: %s", @@ -1814,6 +1818,8 @@ def _process_entry( return region, account_id = extract_region_and_account_id(event_bus_name_or_arn, context) + + # TODO check interference with x-ray trace header if encoded_trace_header := get_trace_header_encoded_region_account( entry, context.region, context.account_id, region, account_id ): @@ -1837,14 +1843,16 @@ def _process_entry( ) return - self._proxy_capture_input_event(event_formatted) + trace_header = context.trace_context["aws_trace_header"] + + self._proxy_capture_input_event(event_formatted, trace_header) # Always add the successful EventId entry, even if target processing might fail processed_entries.append({"EventId": event_formatted["id"]}) if configured_rules := list(event_bus.rules.values()): for rule in configured_rules: - self._process_rules(rule, region, account_id, event_formatted) + self._process_rules(rule, region, account_id, event_formatted, trace_header) else: LOG.info( json.dumps( @@ -1855,7 +1863,7 @@ def _process_entry( ) ) - def _proxy_capture_input_event(self, event: FormattedEvent) -> None: + def _proxy_capture_input_event(self, event: FormattedEvent, trace_header: TraceHeader) -> None: # only required for eventstudio to capture input event if no rule is configured pass @@ -1865,6 +1873,7 @@ def _process_rules( region: str, account_id: str, event_formatted: FormattedEvent, + trace_header: TraceHeader, ) -> None: """Process rules for an event. Note that we no longer handle entries here as AWS returns success regardless of target failures.""" event_pattern = rule.event_pattern @@ -1894,7 +1903,7 @@ def _process_rules( target_unique_id = f"{rule.arn}-{target_id}" target_sender = self._target_sender_store[target_unique_id] try: - target_sender.process_event(event_formatted.copy()) + target_sender.process_event(event_formatted.copy(), trace_header) rule_invocation.labels( status=InvocationStatus.success, service=target_sender.service, diff --git a/localstack-core/localstack/services/events/target.py b/localstack-core/localstack/services/events/target.py index b12691f28925e..fe18ce999412c 100644 --- a/localstack-core/localstack/services/events/target.py +++ b/localstack-core/localstack/services/events/target.py @@ -47,6 +47,7 @@ from localstack.utils.json import extract_jsonpath from localstack.utils.strings import to_bytes from localstack.utils.time import now_utc +from localstack.utils.xray.trace_header import TraceHeader LOG = logging.getLogger(__name__) @@ -63,6 +64,7 @@ ) TRANSFORMER_PLACEHOLDER_PATTERN = re.compile(r"<(.*?)>") +TRACE_HEADER_KEY = "X-Amzn-Trace-Id" def transform_event_with_target_input_path( @@ -193,10 +195,10 @@ def client(self): return self._client @abstractmethod - def send_event(self, event: FormattedEvent | TransformedEvent): + def send_event(self, event: FormattedEvent | TransformedEvent, trace_header: TraceHeader): pass - def process_event(self, event: FormattedEvent): + def process_event(self, event: FormattedEvent, trace_header: TraceHeader): """Processes the event and send it to the target.""" if input_ := self.target.get("Input"): event = json.loads(input_) @@ -208,7 +210,7 @@ def process_event(self, event: FormattedEvent): if input_transformer := self.target.get("InputTransformer"): event = self.transform_event_with_target_input_transformer(input_transformer, event) if event: - self.send_event(event) + self.send_event(event, trace_header) else: LOG.info("No event to send to target %s", self.target.get("Id")) @@ -257,6 +259,7 @@ def _initialize_client(self) -> BaseClient: client = client.request_metadata( service_principal=service_principal, source_arn=self.rule_arn ) + self._register_client_hooks(client) return client def _validate_input_transformer(self, input_transformer: InputTransformer): @@ -287,6 +290,24 @@ def _get_predefined_template_replacements(self, event: FormattedEvent) -> dict[s return predefined_template_replacements + def _register_client_hooks(self, client: BaseClient): + """Register client hooks to inject trace header into requests.""" + + def handle_extract_params(params, context, **kwargs): + trace_header = params.pop("TraceHeader", None) + if trace_header is None: + return + context[TRACE_HEADER_KEY] = trace_header.to_header_str() + + def handle_inject_headers(params, context, **kwargs): + if trace_header_str := context.pop(TRACE_HEADER_KEY, None): + params["headers"][TRACE_HEADER_KEY] = trace_header_str + + client.meta.events.register( + f"provide-client-params.{self.service}.*", handle_extract_params + ) + client.meta.events.register(f"before-call.{self.service}.*", handle_inject_headers) + TargetSenderDict = dict[str, TargetSender] # rule_arn-target_id as global unique id @@ -316,7 +337,7 @@ class ApiGatewayTargetSender(TargetSender): ALLOWED_HTTP_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"} - def send_event(self, event): + def send_event(self, event, trace_header): # Parse the ARN to extract api_id, stage_name, http_method, and resource path # Example ARN: arn:{partition}:execute-api:{region}:{account_id}:{api_id}/{stage_name}/{method}/{resource_path} arn_parts = parse_arn(self.target["Arn"]) @@ -383,6 +404,9 @@ def send_event(self, event): # Serialize the event, converting datetime objects to strings event_json = json.dumps(event, default=str) + # Add trace header + headers[TRACE_HEADER_KEY] = trace_header.to_header_str() + # Send the HTTP request response = requests.request( method=http_method, url=url, headers=headers, data=event_json, timeout=5 @@ -415,12 +439,12 @@ def _get_predefined_template_replacements(self, event: Dict[str, Any]) -> Dict[s class AppSyncTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("AppSync target is not yet implemented") class BatchTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("Batch target is not yet implemented") def _validate_input(self, target: Target): @@ -433,7 +457,7 @@ def _validate_input(self, target: Target): class ECSTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("ECS target is a pro feature, please use LocalStack Pro") def _validate_input(self, target: Target): @@ -444,7 +468,7 @@ def _validate_input(self, target: Target): class EventsTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): # TODO add validation and tests for eventbridge to eventbridge requires Detail, DetailType, and Source # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/events/client/put_events.html source = self._get_source(event) @@ -464,7 +488,8 @@ def send_event(self, event): event, self.region, self.account_id, self.target_region, self.target_account_id ): entries[0]["TraceHeader"] = encoded_original_id - self.client.put_events(Entries=entries) + + self.client.put_events(Entries=entries, TraceHeader=trace_header) def _get_source(self, event: FormattedEvent | TransformedEvent) -> str: if isinstance(event, dict) and (source := event.get("source")): @@ -486,7 +511,7 @@ def _get_resources(self, event: FormattedEvent | TransformedEvent) -> list[str]: class EventsApiDestinationTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): """Send an event to an EventBridge API destination See https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html""" target_arn = self.target["Arn"] @@ -520,6 +545,9 @@ def send_event(self, event): if http_parameters := self.target.get("HttpParameters"): endpoint = add_target_http_parameters(http_parameters, endpoint, headers, event) + # add trace header + headers[TRACE_HEADER_KEY] = trace_header.to_header_str() + result = requests.request( method=method, url=endpoint, data=json.dumps(event or {}), headers=headers ) @@ -532,8 +560,9 @@ def send_event(self, event): class FirehoseTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): delivery_stream_name = firehose_name(self.target["Arn"]) + self.client.put_record( DeliveryStreamName=delivery_stream_name, Record={"Data": to_bytes(to_json_str(event))}, @@ -541,7 +570,7 @@ def send_event(self, event): class KinesisTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): partition_key_path = collections.get_safe( self.target, "$.KinesisParameters.PartitionKeyPath", @@ -549,6 +578,7 @@ def send_event(self, event): ) stream_name = self.target["Arn"].split("/")[-1] partition_key = collections.get_safe(event, partition_key_path, event["id"]) + self.client.put_record( StreamName=stream_name, Data=to_bytes(to_json_str(event)), @@ -565,18 +595,20 @@ def _validate_input(self, target: Target): class LambdaTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): self.client.invoke( FunctionName=self.target["Arn"], Payload=to_bytes(to_json_str(event)), InvocationType="Event", + TraceHeader=trace_header, ) class LogsTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): log_group_name = self.target["Arn"].split(":")[6] log_stream_name = str(uuid.uuid4()) # Unique log stream name + self.client.create_log_stream(logGroupName=log_group_name, logStreamName=log_stream_name) self.client.put_log_events( logGroupName=log_group_name, @@ -591,7 +623,7 @@ def send_event(self, event): class RedshiftTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("Redshift target is not yet implemented") def _validate_input(self, target: Target): @@ -602,20 +634,21 @@ def _validate_input(self, target: Target): class SagemakerTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("Sagemaker target is not yet implemented") class SnsTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): self.client.publish(TopicArn=self.target["Arn"], Message=to_json_str(event)) class SqsTargetSender(TargetSender): - def send_event(self, event): + def send_event(self, event, trace_header): queue_url = sqs_queue_url_for_arn(self.target["Arn"]) msg_group_id = self.target.get("SqsParameters", {}).get("MessageGroupId", None) kwargs = {"MessageGroupId": msg_group_id} if msg_group_id else {} + self.client.send_message( QueueUrl=queue_url, MessageBody=to_json_str(event), @@ -626,8 +659,9 @@ def send_event(self, event): class StatesTargetSender(TargetSender): """Step Functions Target Sender""" - def send_event(self, event): + def send_event(self, event, trace_header): self.service = "stepfunctions" + self.client.start_execution( stateMachineArn=self.target["Arn"], name=event["id"], input=to_json_str(event) ) @@ -642,7 +676,7 @@ def _validate_input(self, target: Target): class SystemsManagerSender(TargetSender): """EC2 Run Command Target Sender""" - def send_event(self, event): + def send_event(self, event, trace_header): raise NotImplementedError("Systems Manager target is not yet implemented") def _validate_input(self, target: Target): diff --git a/tests/aws/services/events/test_x_ray_trace_propagation.py b/tests/aws/services/events/test_x_ray_trace_propagation.py new file mode 100644 index 0000000000000..a894dc6345b7d --- /dev/null +++ b/tests/aws/services/events/test_x_ray_trace_propagation.py @@ -0,0 +1,434 @@ +import json +import time + +from localstack.aws.api.lambda_ import Runtime +from localstack.testing.pytest import markers +from localstack.utils.strings import short_uid +from localstack.utils.sync import retry +from localstack.utils.testutil import check_expected_lambda_log_events_length +from localstack.utils.xray.trace_header import TraceHeader +from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_AWS_PROXY_FORMAT + +APIGATEWAY_ASSUME_ROLE_POLICY = { + "Statement": { + "Sid": "", + "Effect": "Allow", + "Principal": {"Service": "apigateway.amazonaws.com"}, + "Action": "sts:AssumeRole", + } +} +import re + +import pytest + +from localstack.testing.aws.util import is_aws_cloud +from tests.aws.services.events.helper_functions import is_old_provider +from tests.aws.services.events.test_events import TEST_EVENT_DETAIL, TEST_EVENT_PATTERN +from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_XRAY_TRACEID + +# currently only API Gateway v2 and Lambda support X-Ray tracing + + +@markers.aws.validated +@pytest.mark.skipif( + condition=is_old_provider(), + reason="not supported by the old provider", +) +def test_xray_trace_propagation_events_api_gateway( + aws_client, + create_role_with_policy, + create_lambda_function, + create_rest_apigw, + events_create_event_bus, + events_put_rule, + region_name, + cleanups, + account_id, +): + # create lambda + function_name = f"test-function-{short_uid()}" + function_arn = create_lambda_function( + func_name=function_name, + handler_file=TEST_LAMBDA_AWS_PROXY_FORMAT, + handler="lambda_aws_proxy_format.handler", + runtime=Runtime.python3_12, + )["CreateFunctionResponse"]["FunctionArn"] + + # create api gateway with lambda integration + # create rest api + api_id, api_name, root_id = create_rest_apigw( + name=f"test-api-{short_uid()}", + description="Test Integration with EventBridge X-Ray", + ) + + resource_id = aws_client.apigateway.create_resource( + restApiId=api_id, parentId=root_id, pathPart="test" + )["id"] + + aws_client.apigateway.put_method( + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + authorizationType="NONE", + ) + + # Lambda AWS_PROXY integration + aws_client.apigateway.put_integration( + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + type="AWS_PROXY", + integrationHttpMethod="POST", + uri=f"arn:aws:apigateway:{region_name}:lambda:path/2015-03-31/functions/{function_arn}/invocations", + ) + + # Give permission to API Gateway to invoke Lambda + source_arn = f"arn:aws:execute-api:{region_name}:{account_id}:{api_id}/*/POST/test" + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId=f"sid-{short_uid()}", + Action="lambda:InvokeFunction", + Principal="apigateway.amazonaws.com", + SourceArn=source_arn, + ) + + stage_name = "test" + aws_client.apigateway.create_deployment(restApiId=api_id, stageName=stage_name) + + # Create event bus + event_bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=event_bus_name) + + # Create rule + rule_name = f"test-rule-{short_uid()}" + event_pattern = {"source": ["test.source"], "detail-type": ["test.detail.type"]} + events_put_rule( + Name=rule_name, + EventBusName=event_bus_name, + EventPattern=json.dumps(event_pattern), + ) + + # Create an IAM Role for EventBridge to invoke API Gateway + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "events.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + + role_name, role_arn = create_role_with_policy( + effect="Allow", + actions="execute-api:Invoke", + assume_policy_doc=json.dumps(assume_role_policy_document), + resource=source_arn, + attach=False, # Since we're using put_role_policy, not attach_role_policy + ) + + # Allow some time for IAM role propagation (only needed in AWS) + if is_aws_cloud(): + time.sleep(10) + + # Add the API Gateway as a target with the RoleArn + target_id = f"target-{short_uid()}" + api_target_arn = ( + f"arn:aws:execute-api:{region_name}:{account_id}:{api_id}/{stage_name}/POST/test" + ) + put_targets_response = aws_client.events.put_targets( + Rule=rule_name, + EventBusName=event_bus_name, + Targets=[ + { + "Id": target_id, + "Arn": api_target_arn, + "RoleArn": role_arn, + "Input": json.dumps({"message": "Hello from EventBridge"}), + "RetryPolicy": {"MaximumRetryAttempts": 0}, + } + ], + ) + assert put_targets_response["FailedEntryCount"] == 0 + + ###### + # Test + ###### + # Enable X-Ray tracing for the aws_client + trace_id = "1-67f4141f-e1cd7672871da115129f8b19" + parent_id = "d0ee9531727135a0" + xray_trace_header = TraceHeader(root=trace_id, parent=parent_id, sampled=1) + + def add_xray_header(request, **kwargs): + request.headers["X-Amzn-Trace-Id"] = xray_trace_header.to_header_str() + + event_name = "before-send.events.*" + aws_client.events.meta.events.register(event_name, add_xray_header) + + # make sure the hook gets cleaned up after the test + cleanups.append(lambda: aws_client.events.meta.events.unregister(event_name, add_xray_header)) + + event_entry = { + "EventBusName": event_bus_name, + "Source": "test.source", + "DetailType": "test.detail.type", + "Detail": json.dumps({"message": "Hello from EventBridge"}), + } + put_events_response = aws_client.events.put_events(Entries=[event_entry]) + assert put_events_response["FailedEntryCount"] == 0 + + # Verify the Lambda invocation + events = retry( + check_expected_lambda_log_events_length, + retries=10, + sleep=10, + sleep_before=10 if is_aws_cloud() else 1, + function_name=function_name, + expected_length=1, + logs_client=aws_client.logs, + ) + + # TODO how to assert X-Ray trace ID correct propagation from eventbridge to api gateway if no X-Ray trace id is present in the event + + lambda_trace_header = events[0]["headers"].get("X-Amzn-Trace-Id") + assert lambda_trace_header is not None + lambda_trace_id = re.search(r"Root=([^;]+)", lambda_trace_header).group(1) + assert lambda_trace_id == trace_id + + +@markers.aws.validated +@pytest.mark.skipif( + condition=is_old_provider(), + reason="not supported by the old provider", +) +def test_xray_trace_propagation_events_lambda( + create_lambda_function, + events_create_event_bus, + events_put_rule, + cleanups, + aws_client, +): + function_name = f"lambda-func-{short_uid()}" + create_lambda_response = create_lambda_function( + handler_file=TEST_LAMBDA_XRAY_TRACEID, + func_name=function_name, + runtime=Runtime.python3_12, + ) + lambda_function_arn = create_lambda_response["CreateFunctionResponse"]["FunctionArn"] + + bus_name = f"bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"rule-{short_uid()}" + rule_arn = events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + )["RuleArn"] + + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId=f"{rule_name}-Event", + Action="lambda:InvokeFunction", + Principal="events.amazonaws.com", + SourceArn=rule_arn, + ) + + target_id = f"target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": target_id, "Arn": lambda_function_arn}], + ) + + # Enable X-Ray tracing for the aws_client + trace_id = "1-67f4141f-e1cd7672871da115129f8b19" + parent_id = "d0ee9531727135a0" + xray_trace_header = TraceHeader(root=trace_id, parent=parent_id, sampled=1) + + def add_xray_header(request, **kwargs): + request.headers["X-Amzn-Trace-Id"] = xray_trace_header.to_header_str() + + event_name = "before-send.events.*" + aws_client.events.meta.events.register(event_name, add_xray_header) + # make sure the hook gets cleaned up after the test + cleanups.append(lambda: aws_client.events.meta.events.unregister(event_name, add_xray_header)) + + aws_client.events.put_events( + Entries=[ + { + "EventBusName": bus_name, + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(TEST_EVENT_DETAIL), + } + ] + ) + + # Verify the Lambda invocation + events = retry( + check_expected_lambda_log_events_length, + retries=10, + sleep=10, + function_name=function_name, + expected_length=1, + logs_client=aws_client.logs, + ) + + # TODO how to assert X-Ray trace ID correct propagation from eventbridge to api lambda if no X-Ray trace id is present in the event + + lambda_trace_header = events[0]["trace_id_inside_handler"] + assert lambda_trace_header is not None + lambda_trace_id = re.search(r"Root=([^;]+)", lambda_trace_header).group(1) + assert lambda_trace_id == trace_id + + +@markers.aws.validated +@pytest.mark.parametrize( + "bus_combination", [("default", "custom"), ("custom", "custom"), ("custom", "default")] +) +@pytest.mark.skipif( + condition=is_old_provider(), + reason="not supported by the old provider", +) +def test_xray_trace_propagation_events_events( + bus_combination, + create_lambda_function, + events_create_event_bus, + create_role_event_bus_source_to_bus_target, + region_name, + account_id, + events_put_rule, + cleanups, + aws_client, +): + """ + Event Bridge Bus Source to Event Bridge Bus Target to Lambda for asserting X-Ray trace propagation + """ + # Create event buses + bus_source, bus_target = bus_combination + if bus_source == "default": + bus_name_source = "default" + if bus_source == "custom": + bus_name_source = f"test-event-bus-source-{short_uid()}" + events_create_event_bus(Name=bus_name_source) + if bus_target == "default": + bus_name_target = "default" + bus_arn_target = f"arn:aws:events:{region_name}:{account_id}:event-bus/default" + if bus_target == "custom": + bus_name_target = f"test-event-bus-target-{short_uid()}" + bus_arn_target = events_create_event_bus(Name=bus_name_target)["EventBusArn"] + + # Create permission for event bus source to send events to event bus target + role_arn_bus_source_to_bus_target = create_role_event_bus_source_to_bus_target() + + if is_aws_cloud(): + time.sleep(10) # required for role propagation + + # Permission for event bus target to receive events from event bus source + aws_client.events.put_permission( + StatementId=f"TargetEventBusAccessPermission{short_uid()}", + EventBusName=bus_name_target, + Action="events:PutEvents", + Principal="*", + ) + + # Create rule source event bus to target + rule_name_source_to_target = f"test-rule-source-to-target-{short_uid()}" + events_put_rule( + Name=rule_name_source_to_target, + EventBusName=bus_name_source, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + # Add target event bus as target + target_id_event_bus_target = f"test-target-source-events-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name_source_to_target, + EventBusName=bus_name_source, + Targets=[ + { + "Id": target_id_event_bus_target, + "Arn": bus_arn_target, + "RoleArn": role_arn_bus_source_to_bus_target, + } + ], + ) + + # Create Lambda function + function_name = f"lambda-func-{short_uid()}" + create_lambda_response = create_lambda_function( + handler_file=TEST_LAMBDA_XRAY_TRACEID, + func_name=function_name, + runtime=Runtime.python3_12, + ) + lambda_function_arn = create_lambda_response["CreateFunctionResponse"]["FunctionArn"] + + # Connect Event Bus Target to Lambda + rule_name_lambda = f"rule-{short_uid()}" + rule_arn_lambda = events_put_rule( + Name=rule_name_lambda, + EventBusName=bus_name_target, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + )["RuleArn"] + + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId=f"{rule_name_lambda}-Event", + Action="lambda:InvokeFunction", + Principal="events.amazonaws.com", + SourceArn=rule_arn_lambda, + ) + + target_id_lambda = f"target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name_lambda, + EventBusName=bus_name_target, + Targets=[{"Id": target_id_lambda, "Arn": lambda_function_arn}], + ) + + ###### + # Test + ###### + + # Enable X-Ray tracing for the aws_client + trace_id = "1-67f4141f-e1cd7672871da115129f8b19" + parent_id = "d0ee9531727135a0" + xray_trace_header = TraceHeader(root=trace_id, parent=parent_id, sampled=1) + + def add_xray_header(request, **kwargs): + request.headers["X-Amzn-Trace-Id"] = xray_trace_header.to_header_str() + + event_name = "before-send.events.*" + aws_client.events.meta.events.register(event_name, add_xray_header) + # make sure the hook gets cleaned up after the test + cleanups.append(lambda: aws_client.events.meta.events.unregister(event_name, add_xray_header)) + + aws_client.events.put_events( + Entries=[ + { + "EventBusName": bus_name_source, + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(TEST_EVENT_DETAIL), + } + ] + ) + + # Verify the Lambda invocation + events = retry( + check_expected_lambda_log_events_length, + retries=10, + sleep=10, + function_name=function_name, + expected_length=1, + logs_client=aws_client.logs, + ) + + # TODO how to assert X-Ray trace ID correct propagation from eventbridge to eventbridge lambda if no X-Ray trace id is present in the event + + lambda_trace_header = events[0]["trace_id_inside_handler"] + assert lambda_trace_header is not None + lambda_trace_id = re.search(r"Root=([^;]+)", lambda_trace_header).group(1) + assert lambda_trace_id == trace_id diff --git a/tests/aws/services/events/test_x_ray_trace_propagation.validation.json b/tests/aws/services/events/test_x_ray_trace_propagation.validation.json new file mode 100644 index 0000000000000..5ce2e5c48fff7 --- /dev/null +++ b/tests/aws/services/events/test_x_ray_trace_propagation.validation.json @@ -0,0 +1,17 @@ +{ + "tests/aws/services/events/test_x_ray_trace_propagation.py::test_xray_trace_propagation_events_api_gateway": { + "last_validated_date": "2025-04-08T10:51:26+00:00" + }, + "tests/aws/services/events/test_x_ray_trace_propagation.py::test_xray_trace_propagation_events_events[bus_combination0]": { + "last_validated_date": "2025-04-10T10:13:06+00:00" + }, + "tests/aws/services/events/test_x_ray_trace_propagation.py::test_xray_trace_propagation_events_events[bus_combination1]": { + "last_validated_date": "2025-04-10T10:13:27+00:00" + }, + "tests/aws/services/events/test_x_ray_trace_propagation.py::test_xray_trace_propagation_events_events[bus_combination2]": { + "last_validated_date": "2025-04-10T10:14:01+00:00" + }, + "tests/aws/services/events/test_x_ray_trace_propagation.py::test_xray_trace_propagation_events_lambda": { + "last_validated_date": "2025-04-08T10:46:50+00:00" + } +} diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index 700361d32ebbb..205d2e9c1f113 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -128,6 +128,7 @@ ) TEST_LAMBDA_NOTIFIER = os.path.join(THIS_FOLDER, "functions/lambda_notifier.py") TEST_LAMBDA_CLOUDWATCH_LOGS = os.path.join(THIS_FOLDER, "functions/lambda_cloudwatch_logs.py") +TEST_LAMBDA_XRAY_TRACEID = os.path.join(THIS_FOLDER, "functions/xray_tracing_traceid.py") PYTHON_TEST_RUNTIMES = RUNTIMES_AGGREGATED["python"] NODE_TEST_RUNTIMES = RUNTIMES_AGGREGATED["nodejs"] From 22e82dc91b32ee45d74def6a41e72939be64ef77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 08:19:30 +0200 Subject: [PATCH 052/108] Bump python from 3.11.11-slim-bookworm to 3.11.12-slim-bookworm in the docker-base-images group (#12523) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- Dockerfile.s3 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6c5fa4906f3a7..d75a5b7205db3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # # base: Stage which installs necessary runtime dependencies (OS packages, etc.) # -FROM python:3.11.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf19cd45e6a52d541e6c178f0 AS base +FROM python:3.11.12-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index c128c8690228e..98e82f396e9d3 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf19cd45e6a52d541e6c178f0 AS base +FROM python:3.11.12-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS base ARG TARGETARCH # set workdir From a86bbcaf01f6ce5f16f4fe973487bd4e45b07285 Mon Sep 17 00:00:00 2001 From: Jim Wilkinson Date: Tue, 15 Apr 2025 08:34:23 +0100 Subject: [PATCH 053/108] Allow --profile to be specified anywhere on the CLI command line (#12500) Prior to this commit, --profile must be specified as a top-level option, e.g. localstack --profile test start. Now it can be specified at any point, e.g. localstack start --profile test. --- localstack-core/localstack/cli/main.py | 5 +- localstack-core/localstack/cli/profiles.py | 52 ++++++-- tests/unit/cli/test_profiles.py | 148 +++++++++++++++++++-- 3 files changed, 181 insertions(+), 24 deletions(-) diff --git a/localstack-core/localstack/cli/main.py b/localstack-core/localstack/cli/main.py index d9162bb098a4d..de1f04e38cac5 100644 --- a/localstack-core/localstack/cli/main.py +++ b/localstack-core/localstack/cli/main.py @@ -6,9 +6,10 @@ def main(): os.environ["LOCALSTACK_CLI"] = "1" # config profiles are the first thing that need to be loaded (especially before localstack.config!) - from .profiles import set_profile_from_sys_argv + from .profiles import set_and_remove_profile_from_sys_argv - set_profile_from_sys_argv() + # WARNING: This function modifies sys.argv to remove the profile argument. + set_and_remove_profile_from_sys_argv() # initialize CLI plugins from .localstack import create_with_plugins diff --git a/localstack-core/localstack/cli/profiles.py b/localstack-core/localstack/cli/profiles.py index 1625b802f73a4..585757496e08c 100644 --- a/localstack-core/localstack/cli/profiles.py +++ b/localstack-core/localstack/cli/profiles.py @@ -1,3 +1,4 @@ +import argparse import os import sys from typing import Optional @@ -5,36 +6,61 @@ # important: this needs to be free of localstack imports -def set_profile_from_sys_argv(): +def set_and_remove_profile_from_sys_argv(): """ - Reads the --profile flag from sys.argv and then sets the 'CONFIG_PROFILE' os variable accordingly. This is later - picked up by ``localstack.config``. + Performs the following steps: + + 1. Use argparse to parse the command line arguments for the --profile flag. + All occurrences are removed from the sys.argv list, and the value from + the last occurrence is used. This allows the user to specify a profile + at any point on the command line. + + 2. If a --profile flag is not found, check for the -p flag. The first + occurrence of the -p flag is used and it is not removed from sys.argv. + The reasoning for this is that at least one of the CLI subcommands has + a -p flag, and we want to keep it in sys.argv for that command to + pick up. An existing bug means that if a -p flag is used with a + subcommand, it could erroneously be used as the profile value as well. + This behaviour is undesired, but we must maintain back-compatibility of + allowing the profile to be specified using -p. + + 3. If a profile is found, the 'CONFIG_PROFILE' os variable is set + accordingly. This is later picked up by ``localstack.config``. + + WARNING: Any --profile options are REMOVED from sys.argv, so that they are + not passed to the localstack CLI. This allows the profile option + to be set at any point on the command line. """ - profile = parse_profile_argument(sys.argv) + parser = argparse.ArgumentParser() + parser.add_argument("--profile") + namespace, sys.argv = parser.parse_known_args(sys.argv) + profile = namespace.profile + + if not profile: + # if no profile is given, check for the -p argument + profile = parse_p_argument(sys.argv) + if profile: os.environ["CONFIG_PROFILE"] = profile.strip() -def parse_profile_argument(args) -> Optional[str]: +def parse_p_argument(args) -> Optional[str]: """ - Lightweight arg parsing to find ``--profile ``, or ``--profile=`` and return the value of + Lightweight arg parsing to find the first occurrence of ``-p ``, or ``-p=`` and return the value of ```` from the given arguments. :param args: list of CLI arguments - :returns: the value of ``--profile``. + :returns: the value of ``-p``. """ for i, current_arg in enumerate(args): - if current_arg.startswith("--profile="): - # if using the "=" notation, we remove the "--profile=" prefix to get the value - return current_arg[10:] - elif current_arg.startswith("-p="): + if current_arg.startswith("-p="): # if using the "=" notation, we remove the "-p=" prefix to get the value return current_arg[3:] - if current_arg in ["--profile", "-p"]: + if current_arg == "-p": # otherwise use the next arg in the args list as value try: return args[i + 1] - except KeyError: + except IndexError: return None return None diff --git a/tests/unit/cli/test_profiles.py b/tests/unit/cli/test_profiles.py index d519fe73b2609..c48fd4b9e739d 100644 --- a/tests/unit/cli/test_profiles.py +++ b/tests/unit/cli/test_profiles.py @@ -1,18 +1,148 @@ import os import sys -from localstack.cli.profiles import set_profile_from_sys_argv +from localstack.cli.profiles import set_and_remove_profile_from_sys_argv -def test_profiles_equals_notation(monkeypatch): - monkeypatch.setattr(sys, "argv", ["--profile=non-existing-test-profile"]) +def profile_test(monkeypatch, input_args, expected_profile, expected_argv): + monkeypatch.setattr(sys, "argv", input_args) monkeypatch.setenv("CONFIG_PROFILE", "") - set_profile_from_sys_argv() - assert os.environ["CONFIG_PROFILE"] == "non-existing-test-profile" + set_and_remove_profile_from_sys_argv() + assert os.environ["CONFIG_PROFILE"] == expected_profile + assert sys.argv == expected_argv + + +def test_profiles_equals_notation(monkeypatch): + profile_test( + monkeypatch, + input_args=["--profile=non-existing-test-profile"], + expected_profile="non-existing-test-profile", + expected_argv=[], + ) def test_profiles_separate_args_notation(monkeypatch): - monkeypatch.setattr(sys, "argv", ["--profile", "non-existing-test-profile"]) - monkeypatch.setenv("CONFIG_PROFILE", "") - set_profile_from_sys_argv() - assert os.environ["CONFIG_PROFILE"] == "non-existing-test-profile" + profile_test( + monkeypatch, + input_args=["--profile", "non-existing-test-profile"], + expected_profile="non-existing-test-profile", + expected_argv=[], + ) + + +def test_p_equals_notation(monkeypatch): + profile_test( + monkeypatch, + input_args=["-p=non-existing-test-profile"], + expected_profile="non-existing-test-profile", + expected_argv=["-p=non-existing-test-profile"], + ) + + +def test_p_separate_args_notation(monkeypatch): + profile_test( + monkeypatch, + input_args=["-p", "non-existing-test-profile"], + expected_profile="non-existing-test-profile", + expected_argv=["-p", "non-existing-test-profile"], + ) + + +def test_profiles_args_before_and_after(monkeypatch): + profile_test( + monkeypatch, + input_args=["cli", "-D", "--profile=non-existing-test-profile", "start"], + expected_profile="non-existing-test-profile", + expected_argv=["cli", "-D", "start"], + ) + + +def test_profiles_args_before_and_after_separate(monkeypatch): + profile_test( + monkeypatch, + input_args=["cli", "-D", "--profile", "non-existing-test-profile", "start"], + expected_profile="non-existing-test-profile", + expected_argv=["cli", "-D", "start"], + ) + + +def test_p_args_before_and_after_separate(monkeypatch): + profile_test( + monkeypatch, + input_args=["cli", "-D", "-p", "non-existing-test-profile", "start"], + expected_profile="non-existing-test-profile", + expected_argv=["cli", "-D", "-p", "non-existing-test-profile", "start"], + ) + + +def test_profiles_args_multiple(monkeypatch): + profile_test( + monkeypatch, + input_args=[ + "cli", + "--profile", + "non-existing-test-profile", + "start", + "--profile", + "another-profile", + ], + expected_profile="another-profile", + expected_argv=["cli", "start"], + ) + + +def test_p_args_multiple(monkeypatch): + profile_test( + monkeypatch, + input_args=[ + "cli", + "-p", + "non-existing-test-profile", + "start", + "-p", + "another-profile", + ], + expected_profile="non-existing-test-profile", + expected_argv=[ + "cli", + "-p", + "non-existing-test-profile", + "start", + "-p", + "another-profile", + ], + ) + + +def test_p_and_profile_args(monkeypatch): + profile_test( + monkeypatch, + input_args=[ + "cli", + "-p", + "non-existing-test-profile", + "start", + "--profile", + "the_profile", + "-p", + "another-profile", + ], + expected_profile="the_profile", + expected_argv=[ + "cli", + "-p", + "non-existing-test-profile", + "start", + "-p", + "another-profile", + ], + ) + + +def test_trailing_p_argument(monkeypatch): + profile_test( + monkeypatch, + input_args=["cli", "start", "-p"], + expected_profile="", + expected_argv=["cli", "start", "-p"], + ) From fcac2f4b71e2798cc450e1fcbaf37a3be16e15f6 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:34:36 +0200 Subject: [PATCH 054/108] Upgrade dependencies, remove pytest-httpserver patch (#12524) Co-authored-by: LocalStack Bot Co-authored-by: Alexander Rashed --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 6 ++--- requirements-basic.txt | 2 +- requirements-dev.txt | 28 +++++++++++----------- requirements-runtime.txt | 16 ++++++------- requirements-test.txt | 26 ++++++++++----------- requirements-typehint.txt | 44 +++++++++++++++++------------------ tests/conftest.py | 38 ------------------------------ 8 files changed, 62 insertions(+), 100 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45f11e9774cf8..7213524a4b045 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.4 + rev: v0.11.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index db527687b8e99..ec58c4c368b22 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -9,7 +9,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -awscrt==0.25.7 +awscrt==0.26.1 # via localstack-core (pyproject.toml) boto3==1.37.28 # via localstack-core (pyproject.toml) @@ -180,13 +180,13 @@ six==1.17.0 # rfc3339-validator tailer==0.4.1 # via localstack-core (pyproject.toml) -typing-extensions==4.13.1 +typing-extensions==4.13.2 # via # localstack-twisted # pyopenssl # readerwriterlock # referencing -urllib3==2.3.0 +urllib3==2.4.0 # via # botocore # docker diff --git a/requirements-basic.txt b/requirements-basic.txt index 1dc271fc98481..afefd552e94a1 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -54,5 +54,5 @@ semver==3.0.4 # via localstack-core (pyproject.toml) tailer==0.4.1 # via localstack-core (pyproject.toml) -urllib3==2.3.0 +urllib3==2.4.0 # via requests diff --git a/requirements-dev.txt b/requirements-dev.txt index bd777d171521a..ab2f7558364da 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.231 +aws-cdk-asset-awscli-v1==2.2.232 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.188.0 +aws-cdk-lib==2.189.1 # via localstack-core -aws-sam-translator==1.96.0 +aws-sam-translator==1.97.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.7 +awscrt==0.26.1 # via localstack-core boto3==1.37.28 # via @@ -82,7 +82,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.4 +cfn-lint==1.33.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -102,7 +102,7 @@ coverage==7.8.0 # localstack-core coveralls==4.0.1 # via localstack-core (pyproject.toml) -crontab==1.0.1 +crontab==1.0.4 # via localstack-core cryptography==44.0.2 # via @@ -160,7 +160,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.7 +httpcore==1.0.8 # via httpx httpx==0.28.1 # via localstack-core @@ -275,7 +275,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.1 +orderly-set==5.4.0 # via deepdiff packaging==24.2 # via @@ -331,13 +331,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.2 +pydantic==2.11.3 # via aws-sam-translator pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich -pymongo==4.11.3 +pymongo==4.12.0 # via localstack-core pyopenssl==25.0.0 # via @@ -355,7 +355,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.2 +pytest-httpserver==1.1.3 # via localstack-core pytest-rerunfailures==15.0 # via localstack-core @@ -425,7 +425,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.11.4 +ruff==0.11.5 # via localstack-core (pyproject.toml) s3transfer==0.11.4 # via @@ -457,7 +457,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.13.1 +typing-extensions==4.13.2 # via # anyio # aws-sam-translator @@ -472,7 +472,7 @@ typing-extensions==4.13.1 # typing-inspection typing-inspection==0.4.0 # via pydantic -urllib3==2.3.0 +urllib3==2.4.0 # via # botocore # docker diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 40fcaf5d0a44d..ff7a9a6826073 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -23,7 +23,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-sam-translator==1.96.0 +aws-sam-translator==1.97.0 # via # cfn-lint # localstack-core (pyproject.toml) @@ -31,7 +31,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core (pyproject.toml) -awscrt==0.25.7 +awscrt==0.26.1 # via localstack-core boto3==1.37.28 # via @@ -64,7 +64,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.4 +cfn-lint==1.33.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -76,7 +76,7 @@ colorama==0.4.6 # via awscli constantly==23.10.4 # via localstack-twisted -crontab==1.0.1 +crontab==1.0.4 # via localstack-core (pyproject.toml) cryptography==44.0.2 # via @@ -239,13 +239,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.2 +pydantic==2.11.3 # via aws-sam-translator pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich -pymongo==4.11.3 +pymongo==4.12.0 # via localstack-core (pyproject.toml) pyopenssl==25.0.0 # via @@ -332,7 +332,7 @@ tailer==0.4.1 # via # localstack-core # localstack-core (pyproject.toml) -typing-extensions==4.13.1 +typing-extensions==4.13.2 # via # aws-sam-translator # cfn-lint @@ -345,7 +345,7 @@ typing-extensions==4.13.1 # typing-inspection typing-inspection==0.4.0 # via pydantic -urllib3==2.3.0 +urllib3==2.4.0 # via # botocore # docker diff --git a/requirements-test.txt b/requirements-test.txt index d51792859d9a2..7e38839b9f68f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.231 +aws-cdk-asset-awscli-v1==2.2.232 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.188.0 +aws-cdk-lib==2.189.1 # via localstack-core (pyproject.toml) -aws-sam-translator==1.96.0 +aws-sam-translator==1.97.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.7 +awscrt==0.26.1 # via localstack-core boto3==1.37.28 # via @@ -80,7 +80,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.32.4 +cfn-lint==1.33.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -96,7 +96,7 @@ constructs==10.4.2 # via aws-cdk-lib coverage==7.8.0 # via localstack-core (pyproject.toml) -crontab==1.0.1 +crontab==1.0.4 # via localstack-core cryptography==44.0.2 # via @@ -146,7 +146,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.7 +httpcore==1.0.8 # via httpx httpx==0.28.1 # via localstack-core (pyproject.toml) @@ -254,7 +254,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.1 +orderly-set==5.4.0 # via deepdiff packaging==24.2 # via @@ -301,13 +301,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.2 +pydantic==2.11.3 # via aws-sam-translator pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich -pymongo==4.11.3 +pymongo==4.12.0 # via localstack-core pyopenssl==25.0.0 # via @@ -323,7 +323,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.2 +pytest-httpserver==1.1.3 # via localstack-core (pyproject.toml) pytest-rerunfailures==15.0 # via localstack-core (pyproject.toml) @@ -419,7 +419,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.13.1 +typing-extensions==4.13.2 # via # anyio # aws-sam-translator @@ -434,7 +434,7 @@ typing-extensions==4.13.1 # typing-inspection typing-inspection==0.4.0 # via pydantic -urllib3==2.3.0 +urllib3==2.4.0 # via # botocore # docker diff --git a/requirements-typehint.txt b/requirements-typehint.txt index c73806f88c658..a2e55fdef1249 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -27,15 +27,15 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.231 +aws-cdk-asset-awscli-v1==2.2.232 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.188.0 +aws-cdk-lib==2.189.1 # via localstack-core -aws-sam-translator==1.96.0 +aws-sam-translator==1.97.0 # via # cfn-lint # localstack-core @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.38.28 # via localstack-core -awscrt==0.25.7 +awscrt==0.26.1 # via localstack-core boto3==1.37.28 # via @@ -51,7 +51,7 @@ boto3==1.37.28 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.37.29 +boto3-stubs==1.37.34 # via localstack-core (pyproject.toml) botocore==1.37.28 # via @@ -86,7 +86,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.32.4 +cfn-lint==1.33.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -106,7 +106,7 @@ coverage==7.8.0 # localstack-core coveralls==4.0.1 # via localstack-core -crontab==1.0.1 +crontab==1.0.4 # via localstack-core cryptography==44.0.2 # via @@ -164,7 +164,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.7 +httpcore==1.0.8 # via httpx httpx==0.28.1 # via localstack-core @@ -274,7 +274,7 @@ mypy-boto3-appconfig==1.37.0 # via boto3-stubs mypy-boto3-appconfigdata==1.37.0 # via boto3-stubs -mypy-boto3-application-autoscaling==1.37.0 +mypy-boto3-application-autoscaling==1.37.32 # via boto3-stubs mypy-boto3-appsync==1.37.15 # via boto3-stubs @@ -286,7 +286,7 @@ mypy-boto3-backup==1.37.0 # via boto3-stubs mypy-boto3-batch==1.37.22 # via boto3-stubs -mypy-boto3-ce==1.37.10 +mypy-boto3-ce==1.37.30 # via boto3-stubs mypy-boto3-cloudcontrol==1.37.0 # via boto3-stubs @@ -318,7 +318,7 @@ mypy-boto3-dms==1.37.4 # via boto3-stubs mypy-boto3-docdb==1.37.0 # via boto3-stubs -mypy-boto3-dynamodb==1.37.12 +mypy-boto3-dynamodb==1.37.33 # via boto3-stubs mypy-boto3-dynamodbstreams==1.37.0 # via boto3-stubs @@ -332,7 +332,7 @@ mypy-boto3-efs==1.37.0 # via boto3-stubs mypy-boto3-eks==1.37.24 # via boto3-stubs -mypy-boto3-elasticache==1.37.6 +mypy-boto3-elasticache==1.37.32 # via boto3-stubs mypy-boto3-elasticbeanstalk==1.37.0 # via boto3-stubs @@ -352,7 +352,7 @@ mypy-boto3-fis==1.37.0 # via boto3-stubs mypy-boto3-glacier==1.37.0 # via boto3-stubs -mypy-boto3-glue==1.37.29 +mypy-boto3-glue==1.37.31 # via boto3-stubs mypy-boto3-iam==1.37.22 # via boto3-stubs @@ -460,7 +460,7 @@ mypy-boto3-timestream-write==1.37.0 # via boto3-stubs mypy-boto3-transcribe==1.37.27 # via boto3-stubs -mypy-boto3-verifiedpermissions==1.37.0 +mypy-boto3-verifiedpermissions==1.37.33 # via boto3-stubs mypy-boto3-wafv2==1.37.21 # via boto3-stubs @@ -485,7 +485,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core -orderly-set==5.3.1 +orderly-set==5.4.0 # via deepdiff packaging==24.2 # via @@ -541,13 +541,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.2 +pydantic==2.11.3 # via aws-sam-translator pydantic-core==2.33.1 # via pydantic pygments==2.19.1 # via rich -pymongo==4.11.3 +pymongo==4.12.0 # via localstack-core pyopenssl==25.0.0 # via @@ -565,7 +565,7 @@ pytest==8.3.5 # pytest-rerunfailures # pytest-split # pytest-tinybird -pytest-httpserver==1.1.2 +pytest-httpserver==1.1.3 # via localstack-core pytest-rerunfailures==15.0 # via localstack-core @@ -635,7 +635,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.11.4 +ruff==0.11.5 # via localstack-core s3transfer==0.11.4 # via @@ -667,11 +667,11 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -types-awscrt==0.25.7 +types-awscrt==0.26.1 # via botocore-stubs types-s3transfer==0.11.4 # via boto3-stubs -typing-extensions==4.13.1 +typing-extensions==4.13.2 # via # anyio # aws-sam-translator @@ -790,7 +790,7 @@ typing-extensions==4.13.1 # typing-inspection typing-inspection==0.4.0 # via pydantic -urllib3==2.3.0 +urllib3==2.4.0 # via # botocore # docker diff --git a/tests/conftest.py b/tests/conftest.py index 5b9e1edde8f1f..2a23489c537bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,44 +20,6 @@ ] -# FIXME: remove this once https://github.com/csernazs/pytest-httpserver/pull/411 is merged -def pytest_sessionstart(session): - import threading - - try: - from pytest_httpserver import HTTPServer, HTTPServerError - from werkzeug import Request - from werkzeug.serving import make_server - - from localstack.utils.patch import Patch - - def start_non_daemon_thread(self) -> None: - if self.is_running(): - raise HTTPServerError("Server is already running") - - app = Request.application(self.application) - - self.server = make_server( - self.host, - self.port, - app, - ssl_context=self.ssl_context, - threaded=self.threaded, - ) - - self.port = self.server.port # Update port (needed if `port` was set to 0) - self.server_thread = threading.Thread(target=self.thread_target, daemon=True) - self.server_thread.start() - - patch = Patch(name="start", obj=HTTPServer, new=start_non_daemon_thread) - patch.apply() - - except ImportError: - # this will be executed in the CLI tests as well, where we don't have the pytest_httpserver dependency - # skip in that case - pass - - @pytest.fixture(scope="session") def aws_session(): """ From 1954566b7760f5a20e590994f60b8108574a415d Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:01:20 +0200 Subject: [PATCH 055/108] Added GH action to build community image (#12515) --- .github/actions/build-image/action.yml | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/actions/build-image/action.yml diff --git a/.github/actions/build-image/action.yml b/.github/actions/build-image/action.yml new file mode 100644 index 0000000000000..eeb8832cb4494 --- /dev/null +++ b/.github/actions/build-image/action.yml @@ -0,0 +1,63 @@ +name: 'Build Image' +description: 'Composite action which combines all steps necessary to build the LocalStack Community image.' +inputs: + dockerhubPullUsername: + description: 'Username to log in to DockerHub to mitigate rate limiting issues with DockerHub.' + required: false + dockerhubPullToken: + description: 'API token to log in to DockerHub to mitigate rate limiting issues with DockerHub.' + required: false + disableCaching: + description: 'Disable Caching' + required: false +outputs: + image-artifact-name: + description: "Name of the artifact containing the built docker image" + value: ${{ steps.image-artifact-name.outputs.image-artifact-name }} +runs: + using: "composite" + # This GH Action requires localstack repo in 'localstack' dir + full git history (fetch-depth: 0) + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: 'localstack/.python-version' + + - name: Install docker helper dependencies + shell: bash + run: pip install --upgrade setuptools setuptools_scm + + - name: Login to Docker Hub + # login to DockerHub to avoid rate limiting issues on custom runners + uses: docker/login-action@v3 + if: ${{ inputs.dockerHubPullUsername != '' && inputs.dockerHubPullToken != '' }} + with: + username: ${{ inputs.dockerhubPullUsername }} + password: ${{ inputs.dockerhubPullToken }} + + - name: Build Docker Image + id: build-image + shell: bash + env: + DOCKER_BUILD_FLAGS: "--load ${{ inputs.disableCaching == 'true' && '--no-cache' || '' }}" + PLATFORM: ${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }} + DOCKERFILE: ../Dockerfile + DOCKER_BUILD_CONTEXT: .. + IMAGE_NAME: "localstack/localstack" + working-directory: localstack/localstack-core + run: | + ../bin/docker-helper.sh build + ../bin/docker-helper.sh save + + - name: Store Docker Image as Artifact + uses: actions/upload-artifact@v4 + with: + name: localstack-docker-image-${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }} + # the path is defined by the "save" command of the docker-helper, which sets a GitHub output "IMAGE_FILENAME" + path: localstack/localstack-core/${{ steps.build-image.outputs.IMAGE_FILENAME || steps.build-test-image.outputs.IMAGE_FILENAME}} + retention-days: 1 + + - name: Set image artifact name as output + id: image-artifact-name + shell: bash + run: echo "image-artifact-name=localstack-docker-image-${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }}" >> $GITHUB_OUTPUT From 8b4774fc161ca49f4c574f0e0349c1b8b1e65aba Mon Sep 17 00:00:00 2001 From: Alexander Lavesson <35918900+alexlave100@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:39:25 +0200 Subject: [PATCH 056/108] StepFunctions: ListStateMachineAliases pagination support (#12496) Co-authored-by: Giovanni Grano --- .../localstack/services/sqs/provider.py | 3 +- .../localstack/services/sqs/utils.py | 6 - .../services/stepfunctions/backend/alias.py | 4 + .../services/stepfunctions/provider.py | 25 +- .../stepfunctions/stepfunctions_utils.py | 2 +- localstack-core/localstack/utils/strings.py | 6 + tests/aws/services/sqs/test_sqs.py | 6 +- .../stepfunctions/v2/test_sfn_api_aliasing.py | 178 ++++++++++++++ .../v2/test_sfn_api_aliasing.snapshot.json | 232 +++++++++++++++++- .../v2/test_sfn_api_aliasing.validation.json | 44 +++- 10 files changed, 465 insertions(+), 41 deletions(-) diff --git a/localstack-core/localstack/services/sqs/provider.py b/localstack-core/localstack/services/sqs/provider.py index efb857dbbf573..10988383bd745 100644 --- a/localstack-core/localstack/services/sqs/provider.py +++ b/localstack-core/localstack/services/sqs/provider.py @@ -102,7 +102,6 @@ is_fifo_queue, is_message_deduplication_id_required, parse_queue_url, - token_generator, ) from localstack.services.stores import AccountRegionBundle from localstack.utils.aws.arns import parse_arn @@ -116,7 +115,7 @@ from localstack.utils.collections import PaginatedList from localstack.utils.run import FuncThread from localstack.utils.scheduler import Scheduler -from localstack.utils.strings import md5 +from localstack.utils.strings import md5, token_generator from localstack.utils.threads import start_thread from localstack.utils.time import now diff --git a/localstack-core/localstack/services/sqs/utils.py b/localstack-core/localstack/services/sqs/utils.py index 50341e04d7de1..a280128ad7b66 100644 --- a/localstack-core/localstack/services/sqs/utils.py +++ b/localstack-core/localstack/services/sqs/utils.py @@ -184,9 +184,3 @@ def global_message_sequence(): def generate_message_id(): return long_uid() - - -def token_generator(item: str) -> str: - base64_bytes = base64.b64encode(item.encode("utf-8")) - next_token = base64_bytes.decode("utf-8") - return next_token diff --git a/localstack-core/localstack/services/stepfunctions/backend/alias.py b/localstack-core/localstack/services/stepfunctions/backend/alias.py index f6c4995bc7df8..155890abf4cb3 100644 --- a/localstack-core/localstack/services/stepfunctions/backend/alias.py +++ b/localstack-core/localstack/services/stepfunctions/backend/alias.py @@ -11,9 +11,11 @@ Arn, CharacterRestrictedName, DescribeStateMachineAliasOutput, + PageToken, RoutingConfigurationList, StateMachineAliasListItem, ) +from localstack.utils.strings import token_generator class Alias: @@ -25,6 +27,7 @@ class Alias: _state_machine_version_arns: list[Arn] _execution_probability_distribution: list[int] state_machine_alias_arn: Final[Arn] + tokenized_state_machine_alias_arn: Final[PageToken] create_date: datetime.datetime def __init__( @@ -39,6 +42,7 @@ def __init__( self.name = name self._description = None self.state_machine_alias_arn = f"{state_machine_arn}:{name}" + self.tokenized_state_machine_alias_arn = token_generator(self.state_machine_alias_arn) self.update(description=description, routing_configuration_list=routing_configuration_list) self.create_date = self._get_mutex_date() diff --git a/localstack-core/localstack/services/stepfunctions/provider.py b/localstack-core/localstack/services/stepfunctions/provider.py index 40f7bbb6e4483..bf59a7c69949d 100644 --- a/localstack-core/localstack/services/stepfunctions/provider.py +++ b/localstack-core/localstack/services/stepfunctions/provider.py @@ -1057,7 +1057,8 @@ def list_state_machine_aliases( max_results: PageSize = None, **kwargs, ) -> ListStateMachineAliasesOutput: - # TODO: add pagination support. + assert_pagination_parameters_valid(max_results, next_token) + self._validate_state_machine_arn(state_machine_arn) state_machines = self.get_store(context).state_machines state_machine_revision = state_machines.get(state_machine_arn) @@ -1065,11 +1066,31 @@ def list_state_machine_aliases( raise InvalidArn(f"Invalid arn: {state_machine_arn}") state_machine_aliases: StateMachineAliasList = list() + valid_token_found = next_token is None + for alias in state_machine_revision.aliases: state_machine_aliases.append(alias.to_item()) + if alias.tokenized_state_machine_alias_arn == next_token: + valid_token_found = True + + if not valid_token_found: + raise InvalidToken("Invalid Token: 'Invalid token'") + state_machine_aliases.sort(key=lambda item: item["creationDate"]) - return ListStateMachineAliasesOutput(stateMachineAliases=state_machine_aliases) + paginated_list = PaginatedList(state_machine_aliases) + + paginated_aliases, next_token = paginated_list.get_page( + token_generator=lambda item: get_next_page_token_from_arn( + item.get("stateMachineAliasArn") + ), + next_token=next_token, + page_size=100 if max_results == 0 or max_results is None else max_results, + ) + + return ListStateMachineAliasesOutput( + stateMachineAliases=paginated_aliases, nextToken=next_token + ) def list_state_machine_versions( self, diff --git a/localstack-core/localstack/services/stepfunctions/stepfunctions_utils.py b/localstack-core/localstack/services/stepfunctions/stepfunctions_utils.py index a331f44efcd1c..95133b4ed47e8 100644 --- a/localstack-core/localstack/services/stepfunctions/stepfunctions_utils.py +++ b/localstack-core/localstack/services/stepfunctions/stepfunctions_utils.py @@ -46,7 +46,7 @@ def assert_pagination_parameters_valid( next_token: str, next_token_length_limit: int = 1024, max_results_upper_limit: int = 1000, -) -> tuple[int, str]: +) -> None: validation_errors = [] match max_results: diff --git a/localstack-core/localstack/utils/strings.py b/localstack-core/localstack/utils/strings.py index 4c0310f4d3e3a..aead8aaade907 100644 --- a/localstack-core/localstack/utils/strings.py +++ b/localstack-core/localstack/utils/strings.py @@ -238,3 +238,9 @@ def key_value_pairs_to_dict(pairs: str, delimiter: str = ",", separator: str = " """ splits = [split_pair.partition(separator) for split_pair in pairs.split(delimiter)] return {key.strip(): value.strip() for key, _, value in splits} + + +def token_generator(item: str) -> str: + base64_bytes = base64.b64encode(item.encode("utf-8")) + token = base64_bytes.decode("utf-8") + return token diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index 9a562517da77a..af49bd993504a 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -16,10 +16,7 @@ from localstack.services.sqs.constants import DEFAULT_MAXIMUM_MESSAGE_SIZE, SQS_UUID_STRING_SEED from localstack.services.sqs.models import sqs_stores from localstack.services.sqs.provider import MAX_NUMBER_OF_MESSAGES -from localstack.services.sqs.utils import ( - parse_queue_url, - token_generator, -) +from localstack.services.sqs.utils import parse_queue_url from localstack.testing.aws.util import is_aws_cloud from localstack.testing.config import ( SECONDARY_TEST_AWS_ACCESS_KEY_ID, @@ -32,6 +29,7 @@ from localstack.utils.aws.arns import get_partition from localstack.utils.aws.request_context import mock_aws_request_headers from localstack.utils.common import poll_condition, retry, short_uid, short_uid_from_seed, to_str +from localstack.utils.strings import token_generator from localstack.utils.urls import localstack_host from tests.aws.services.lambda_.functions import lambda_integration from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py index abc1d2aff87e7..b1c1b100a9316 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py @@ -1081,3 +1081,181 @@ def test_delete_no_such_alias_arn( sfn_snapshot.match( "delete_state_machine_alias_response", delete_state_machine_alias_response ) + + @markers.aws.validated + def test_list_state_machine_aliases_pagination_invalid_next_token( + self, + create_state_machine_iam_role, + create_state_machine, + create_state_machine_alias, + sfn_snapshot, + aws_client, + ): + sfn_client = aws_client.stepfunctions + + sfn_role_arn = create_state_machine_iam_role(aws_client) + sfn_snapshot.add_transformer(RegexTransformer(sfn_role_arn, "sfn_role_arn")) + + definition = BaseTemplate.load_sfn_template(BaseTemplate.BASE_PASS_RESULT) + + state_machine_name = f"state_machine_{short_uid()}" + create_state_machine_response = create_state_machine( + target_aws_client=aws_client, + name=state_machine_name, + definition=json.dumps(definition), + roleArn=sfn_role_arn, + publish=True, + ) + + sfn_snapshot.add_transformer( + sfn_snapshot.transform.sfn_sm_create_arn(create_state_machine_response, 0) + ) + + state_machine_arn = create_state_machine_response["stateMachineArn"] + state_machine_version_arn = create_state_machine_response["stateMachineVersionArn"] + + state_machine_alias_name = f"AliasName-{short_uid()}" + + sfn_snapshot.add_transformer( + RegexTransformer(state_machine_alias_name, "state_machine_alias_name") + ) + + create_state_machine_alias_response = create_state_machine_alias( + target_aws_client=aws_client, + description="create state machine alias description", + name=state_machine_alias_name, + routingConfiguration=[ + RoutingConfigurationListItem( + stateMachineVersionArn=state_machine_version_arn, weight=100 + ) + ], + ) + + sfn_snapshot.match( + "create_state_machine_alias_response", create_state_machine_alias_response + ) + + state_machine_alias_arn = create_state_machine_alias_response["stateMachineAliasArn"] + await_state_machine_alias_is_created( + stepfunctions_client=sfn_client, + state_machine_arn=state_machine_arn, + state_machine_alias_arn=state_machine_alias_arn, + ) + + with pytest.raises(Exception) as exc: + sfn_client.list_state_machine_aliases( + stateMachineArn=state_machine_arn, nextToken="InvalidToken" + ) + + sfn_snapshot.match( + "invalidTokenException", + {"exception_typename": exc.typename, "exception_value": exc.value}, + ) + + @markers.aws.validated + @pytest.mark.parametrize("max_results", [0, 1]) + def test_list_state_machine_aliases_pagination_max_results( + self, + create_state_machine_iam_role, + create_state_machine, + create_state_machine_alias, + max_results, + sfn_snapshot, + aws_client, + ): + sfn_client = aws_client.stepfunctions + + sfn_role_arn = create_state_machine_iam_role(aws_client) + sfn_snapshot.add_transformer(RegexTransformer(sfn_role_arn, "sfn_role_arn")) + + definition = BaseTemplate.load_sfn_template(BaseTemplate.BASE_PASS_RESULT) + + state_machine_name = f"state_machine_test-{short_uid()}" + create_state_machine_response = create_state_machine( + target_aws_client=aws_client, + name=state_machine_name, + definition=json.dumps(definition), + roleArn=sfn_role_arn, + publish=True, + ) + + sfn_snapshot.add_transformer( + sfn_snapshot.transform.sfn_sm_create_arn(create_state_machine_response, 0) + ) + + state_machine_arn = create_state_machine_response["stateMachineArn"] + state_machine_version_arn = create_state_machine_response["stateMachineVersionArn"] + + for i in range(3): + create_state_machine_alias_response = create_state_machine_alias( + target_aws_client=aws_client, + description=f"Description {i + 1} - create state machine alias", + name=f"AliasName-{i + 1}", + routingConfiguration=[ + RoutingConfigurationListItem( + stateMachineVersionArn=state_machine_version_arn, weight=100 + ) + ], + ) + + sfn_snapshot.match( + f"create_state_machine_alias_response-{i + 1}", create_state_machine_alias_response + ) + + definition["Comment"] = f"Comment {i + 1}" + sfn_client.update_state_machine( + stateMachineArn=state_machine_arn, definition=json.dumps(definition) + ) + + state_machine_alias_arn = create_state_machine_alias_response["stateMachineAliasArn"] + await_state_machine_alias_is_created( + stepfunctions_client=sfn_client, + state_machine_arn=state_machine_arn, + state_machine_alias_arn=state_machine_alias_arn, + ) + + with pytest.raises(Exception) as err: + sfn_client.list_state_machine_aliases(stateMachineArn=state_machine_arn, maxResults=-1) + + sfn_snapshot.match("list_state_machine_aliases_max_results_-1_response", err.value) + + with pytest.raises(Exception) as err: + sfn_client.list_state_machine_aliases( + stateMachineArn=state_machine_arn, maxResults=1001 + ) + + sfn_snapshot.match( + "list_state_machine_aliases_max_results_1001_response", err.value.response + ) + + if max_results == 0: + list_state_machine_aliases_response = sfn_client.list_state_machine_aliases( + stateMachineArn=state_machine_arn, maxResults=0 + ) + + sfn_snapshot.match( + "list_state_machine_aliases_max_results_0_response", + list_state_machine_aliases_response, + ) + + else: + list_state_machine_aliases_response = sfn_client.list_state_machine_aliases( + stateMachineArn=state_machine_arn, maxResults=1 + ) + + sfn_snapshot.match( + "list_state_machine_aliases_max_results_1_response", + list_state_machine_aliases_response, + ) + + list_state_machine_aliases_response = sfn_client.list_state_machine_aliases( + stateMachineArn=state_machine_arn, + nextToken=list_state_machine_aliases_response.get("nextToken"), + ) + + sfn_snapshot.add_transformer(sfn_snapshot.transform.key_value("nextToken")) + + sfn_snapshot.match( + "list_state_machine_aliases_next_token_response", + list_state_machine_aliases_response, + ) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.snapshot.json b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.snapshot.json index d4cd2ff073d7c..2e98c0a3f7842 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_create_alias_single_router_config": { - "recorded-date": "03-03-2025, 13:12:29", + "recorded-date": "09-04-2025, 20:23:57", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -13,7 +13,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_with_state_machine_arn": { - "recorded-date": "03-03-2025, 13:12:48", + "recorded-date": "09-04-2025, 20:24:12", "recorded-content": { "exception": { "exception_typename": "ValidationException", @@ -22,7 +22,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_not_idempotent": { - "recorded-date": "03-03-2025, 13:13:02", + "recorded-date": "09-04-2025, 20:24:29", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -43,7 +43,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_idempotent_create_alias": { - "recorded-date": "03-03-2025, 13:13:16", + "recorded-date": "09-04-2025, 20:24:44", "recorded-content": { "create_state_machine_alias_response_attempt_0": { "creationDate": "datetime", @@ -88,7 +88,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_invalid_router_configs": { - "recorded-date": "03-03-2025, 13:13:32", + "recorded-date": "09-04-2025, 20:25:01", "recorded-content": { "no_routing": { "exception_typename": "ParamValidationError", @@ -125,7 +125,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_invalid_name": { - "recorded-date": "03-03-2025, 13:13:52", + "recorded-date": "09-04-2025, 20:25:16", "recorded-content": { "exception_for_name123": { "exception_typename": "ValidationException", @@ -154,7 +154,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_delete_list": { - "recorded-date": "03-03-2025, 13:14:23", + "recorded-date": "09-04-2025, 20:25:46", "recorded-content": { "list_state_machine_aliases_response_empty": { "stateMachineAliases": [], @@ -291,7 +291,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_invoke_describe_list": { - "recorded-date": "03-03-2025, 13:20:29", + "recorded-date": "09-04-2025, 20:26:23", "recorded-content": { "list_state_machine_aliases_response_empty": { "stateMachineAliases": [], @@ -409,7 +409,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_update_describe": { - "recorded-date": "03-03-2025, 13:15:57", + "recorded-date": "09-04-2025, 20:27:40", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -474,7 +474,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_version_with_alias": { - "recorded-date": "03-03-2025, 13:16:42", + "recorded-date": "09-04-2025, 20:28:26", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -514,7 +514,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_revision_with_alias": { - "recorded-date": "03-03-2025, 13:34:23", + "recorded-date": "09-04-2025, 20:28:41", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -533,7 +533,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_no_such_alias_arn": { - "recorded-date": "03-03-2025, 13:37:24", + "recorded-date": "09-04-2025, 20:28:58", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -552,7 +552,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_update_no_such_alias_arn": { - "recorded-date": "03-03-2025, 15:00:26", + "recorded-date": "09-04-2025, 20:26:04", "recorded-content": { "create_state_machine_alias_response": { "creationDate": "datetime", @@ -567,5 +567,211 @@ "exception_value": "An error occurred (ResourceNotFound) when calling the UpdateStateMachineAlias operation: Request references a resource that does not exist." } } + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_invalid_next_token": { + "recorded-date": "09-04-2025, 20:29:13", + "recorded-content": { + "create_state_machine_alias_response": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::state_machine_alias_name", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "invalidTokenException": { + "exception_typename": "InvalidToken", + "exception_value": "An error occurred (InvalidToken) when calling the ListStateMachineAliases operation: Invalid Token: 'Invalid token'" + } + } + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results_1_next_token": { + "recorded-date": "09-04-2025, 18:46:18", + "recorded-content": { + "create_state_machine_alias_response-1": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-2": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-3": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_state_machine_aliases_max_results_1_response": { + "nextToken": "", + "stateMachineAliases": [ + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_state_machine_aliases_next_token_response": { + "stateMachineAliases": [ + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2" + }, + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results[0]": { + "recorded-date": "09-04-2025, 20:29:33", + "recorded-content": { + "create_state_machine_alias_response-1": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-2": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-3": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_state_machine_aliases_max_results_-1_response": "Parameter validation failed:\nInvalid value for parameter maxResults, value: -1, valid min value: 0", + "list_state_machine_aliases_max_results_1001_response": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value '1001' at 'maxResults' failed to satisfy constraint: Member must have value less than or equal to 1000" + }, + "message": "1 validation error detected: Value '1001' at 'maxResults' failed to satisfy constraint: Member must have value less than or equal to 1000", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "list_state_machine_aliases_max_results_0_response": { + "stateMachineAliases": [ + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1" + }, + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2" + }, + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results[1]": { + "recorded-date": "09-04-2025, 20:29:54", + "recorded-content": { + "create_state_machine_alias_response-1": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-2": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create_state_machine_alias_response-3": { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_state_machine_aliases_max_results_-1_response": "Parameter validation failed:\nInvalid value for parameter maxResults, value: -1, valid min value: 0", + "list_state_machine_aliases_max_results_1001_response": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value '1001' at 'maxResults' failed to satisfy constraint: Member must have value less than or equal to 1000" + }, + "message": "1 validation error detected: Value '1001' at 'maxResults' failed to satisfy constraint: Member must have value less than or equal to 1000", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "list_state_machine_aliases_max_results_1_response": { + "nextToken": "", + "stateMachineAliases": [ + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_state_machine_aliases_next_token_response": { + "stateMachineAliases": [ + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-2" + }, + { + "creationDate": "datetime", + "stateMachineAliasArn": "arn::states::111111111111:stateMachine::AliasName-3" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.validation.json b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.validation.json index 05de14b07fb4f..8768d7579d5d2 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.validation.json +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.validation.json @@ -1,41 +1,59 @@ { "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_create_alias_single_router_config": { - "last_validated_date": "2025-03-03T13:12:29+00:00" + "last_validated_date": "2025-04-09T20:23:57+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_delete_list": { - "last_validated_date": "2025-03-03T13:14:23+00:00" + "last_validated_date": "2025-04-09T20:25:46+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_invoke_describe_list": { - "last_validated_date": "2025-03-03T13:20:29+00:00" + "last_validated_date": "2025-04-09T20:26:23+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_base_lifecycle_create_update_describe": { - "last_validated_date": "2025-03-03T13:15:57+00:00" + "last_validated_date": "2025-04-09T20:27:40+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_no_such_alias_arn": { - "last_validated_date": "2025-03-03T13:37:24+00:00" + "last_validated_date": "2025-04-09T20:28:58+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_revision_with_alias": { - "last_validated_date": "2025-03-03T13:34:23+00:00" + "last_validated_date": "2025-04-09T20:28:41+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_delete_version_with_alias": { - "last_validated_date": "2025-03-03T13:16:42+00:00" + "last_validated_date": "2025-04-09T20:28:26+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_invalid_name": { - "last_validated_date": "2025-03-03T13:13:52+00:00" + "last_validated_date": "2025-04-09T20:25:16+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_invalid_router_configs": { - "last_validated_date": "2025-03-03T13:13:32+00:00" + "last_validated_date": "2025-04-09T20:25:01+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_not_idempotent": { - "last_validated_date": "2025-03-03T13:13:02+00:00" + "last_validated_date": "2025-04-09T20:24:29+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_error_create_alias_with_state_machine_arn": { - "last_validated_date": "2025-03-03T13:12:48+00:00" + "last_validated_date": "2025-04-09T20:24:12+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_idempotent_create_alias": { - "last_validated_date": "2025-03-03T13:13:16+00:00" + "last_validated_date": "2025-04-09T20:24:44+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination": { + "last_validated_date": "2025-04-07T16:51:07+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_invalid_next_token": { + "last_validated_date": "2025-04-09T20:29:13+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results[0]": { + "last_validated_date": "2025-04-09T20:29:33+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results[1]": { + "last_validated_date": "2025-04-09T20:29:54+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results_0": { + "last_validated_date": "2025-04-09T17:36:29+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_list_state_machine_aliases_pagination_max_results_1_next_token": { + "last_validated_date": "2025-04-09T18:46:18+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api_aliasing.py::TestSfnApiAliasing::test_update_no_such_alias_arn": { - "last_validated_date": "2025-03-03T15:00:26+00:00" + "last_validated_date": "2025-04-09T20:26:04+00:00" } } From 2ec35743d8e3c01108d2f75cd33d6014d93d238e Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:51:04 +0200 Subject: [PATCH 057/108] CloudFormation Engine v2: Base Mappings and Conditions tests for Update Graph and PreProc (#12527) --- .../engine/v2/change_set_model_preproc.py | 8 +- .../v2/test_change_set_conditions.py | 180 ++ .../test_change_set_conditions.snapshot.json | 1536 +++++++++++ ...test_change_set_conditions.validation.json | 14 + .../v2/test_change_set_mappings.py | 302 ++ .../v2/test_change_set_mappings.snapshot.json | 2428 +++++++++++++++++ .../test_change_set_mappings.validation.json | 20 + 7 files changed, 4482 insertions(+), 6 deletions(-) create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_conditions.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_conditions.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_conditions.validation.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_mappings.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_mappings.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_mappings.validation.json diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index bc3e6ce07beb9..40c477ce3a545 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -490,10 +490,6 @@ def visit_node_resource( ) condition_before = condition_delta.before condition_after = condition_delta.after - if not condition_before and condition_after: - change_type = ChangeType.CREATED - elif condition_before and not condition_after: - change_type = ChangeType.REMOVED type_delta = self.visit(node_resource.type_) properties_delta: PreprocEntityDelta[PreprocProperties, PreprocProperties] = self.visit( @@ -502,14 +498,14 @@ def visit_node_resource( before = None after = None - if change_type != ChangeType.CREATED: + if change_type != ChangeType.CREATED and condition_before is None or condition_before: before = PreprocResource( name=node_resource.name, condition=condition_before, resource_type=type_delta.before, properties=properties_delta.before, ) - if change_type != ChangeType.REMOVED: + if change_type != ChangeType.REMOVED and condition_after is None or condition_after: after = PreprocResource( name=node_resource.name, condition=condition_after, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_conditions.py b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py new file mode 100644 index 0000000000000..1b597df290238 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py @@ -0,0 +1,180 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) +class TestChangeSetConditions: + @markers.aws.validated + @pytest.mark.skip( + reason="The inclusion of response parameters in executor is in progress, " + "currently it cannot delete due to missing topic arn in the request" + ) + def test_condition_update_removes_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "true"]}}, + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + } + }, + } + template_2 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "false"]}}, + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + }, + "TopicPlaceholder": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name2}, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_condition_update_adds_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "false"]}}, + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + }, + "TopicPlaceholder": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name2}, + }, + }, + } + template_2 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "true"]}}, + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + }, + "TopicPlaceholder": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name2}, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + @pytest.mark.skip( + reason="The inclusion of response parameters in executor is in progress, " + "currently it cannot delete due to missing topic arn in the request" + ) + def test_condition_add_new_negative_condition_to_existent_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1}, + }, + }, + } + template_2 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "false"]}}, + "Resources": { + "SNSTopic": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + }, + "TopicPlaceholder": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name2}, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_condition_add_new_positive_condition_to_existent_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Resources": { + "SNSTopic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1}, + }, + }, + } + template_2 = { + "Conditions": {"CreateTopic": {"Fn::Equals": ["true", "true"]}}, + "Resources": { + "SNSTopic1": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name1}, + }, + "SNSTopic2": { + "Type": "AWS::SNS::Topic", + "Condition": "CreateTopic", + "Properties": {"TopicName": name2}, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_conditions.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_conditions.snapshot.json new file mode 100644 index 0000000000000..147c4f2eae447 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_conditions.snapshot.json @@ -0,0 +1,1536 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_update_removes_resource": { + "recorded-date": "15-04-2025, 13:51:50", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SNSTopic", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SNSTopic": [ + { + "EventId": "SNSTopic-c494ee19-3e85-4cf7-b823-5b706137c086", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-f1a45cee-c917-4856-9b04-fdfa3d210cf3", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_COMPLETE-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "TopicPlaceholder": [ + { + "EventId": "TopicPlaceholder-CREATE_COMPLETE-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_update_adds_resource": { + "recorded-date": "15-04-2025, 14:31:36", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SNSTopic", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SNSTopic": [ + { + "EventId": "SNSTopic-CREATE_COMPLETE-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "TopicPlaceholder": [ + { + "EventId": "TopicPlaceholder-CREATE_COMPLETE-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_add_new_negative_condition_to_existent_resource": { + "recorded-date": "15-04-2025, 15:11:48", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SNSTopic", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "TopicPlaceholder", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SNSTopic": [ + { + "EventId": "SNSTopic-c5786633-a3d3-43cc-8c5d-f504661d0578", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-fb082f5d-2aee-49f6-9eb3-613c40aafad9", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_COMPLETE-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "TopicPlaceholder": [ + { + "EventId": "TopicPlaceholder-CREATE_COMPLETE-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "TopicPlaceholder-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "TopicPlaceholder", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_add_new_positive_condition_to_existent_resource": { + "recorded-date": "15-04-2025, 16:00:40", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SNSTopic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "SNSTopic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SNSTopic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SNSTopic1": [ + { + "EventId": "SNSTopic1-CREATE_COMPLETE-date", + "LogicalResourceId": "SNSTopic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "SNSTopic2": [ + { + "EventId": "SNSTopic2-CREATE_COMPLETE-date", + "LogicalResourceId": "SNSTopic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SNSTopic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SNSTopic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_conditions.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_conditions.validation.json new file mode 100644 index 0000000000000..daba45fdabc59 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_conditions.validation.json @@ -0,0 +1,14 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_add_new_negative_condition_to_existent_resource": { + "last_validated_date": "2025-04-15T15:11:48+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_add_new_positive_condition_to_existent_resource": { + "last_validated_date": "2025-04-15T16:00:39+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_update_adds_resource": { + "last_validated_date": "2025-04-15T14:31:36+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_conditions.py::TestChangeSetConditions::test_condition_update_removes_resource": { + "last_validated_date": "2025-04-15T13:51:50+00:00" + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_mappings.py b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py new file mode 100644 index 0000000000000..fd25328225e41 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py @@ -0,0 +1,302 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) +class TestChangeSetMappings: + @markers.aws.validated + def test_mapping_leaf_update( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + template_1 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + } + }, + } + template_2 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-2"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + } + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_mapping_key_update( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + template_1 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + } + }, + } + template_2 = { + "Mappings": {"SNSMapping": {"KeyNew": {"Val": "display-value-2"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "KeyNew", "Val"]}, + }, + } + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_mapping_addition_with_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + } + }, + } + template_2 = { + "Mappings": { + "SNSMapping": {"Key1": {"Val": "display-value-1", "ValNew": "display-value-new"}} + }, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "ValNew"]}, + }, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_mapping_key_addition_with_resource( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + } + }, + } + template_2 = { + "Mappings": { + "SNSMapping": { + "Key1": { + "Val": "display-value-1", + }, + "Key2": { + "Val": "display-value-1", + "ValNew": "display-value-new", + }, + } + }, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key2", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key2", "ValNew"]}, + }, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_mapping_deletion_with_resource_remap( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Mappings": { + "SNSMapping": {"Key1": {"Val": "display-value-1", "ValNew": "display-value-new"}} + }, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "ValNew"]}, + }, + }, + }, + } + template_2 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_mapping_key_deletion_with_resource_remap( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Mappings": { + "SNSMapping": { + "Key1": { + "Val": "display-value-1", + }, + "Key2": {"Val": "display-value-2"}, + } + }, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key2", "Val"]}, + }, + }, + }, + } + template_2 = { + "Mappings": {"SNSMapping": {"Key1": {"Val": "display-value-1"}}}, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + "DisplayName": {"Fn::FindInMap": ["SNSMapping", "Key1", "Val"]}, + }, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_mappings.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_mappings.snapshot.json new file mode 100644 index 0000000000000..58882da07da49 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_mappings.snapshot.json @@ -0,0 +1,2428 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_leaf_update": { + "recorded-date": "15-04-2025, 13:03:18", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_update": { + "recorded-date": "15-04-2025, 13:04:44", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_addition_with_resource": { + "recorded-date": "15-04-2025, 13:05:52", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_addition_with_resource": { + "recorded-date": "15-04-2025, 13:07:01", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_deletion_with_resource_remap": { + "recorded-date": "15-04-2025, 13:08:27", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-1", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-new", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-new", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_deletion_with_resource_remap": { + "recorded-date": "15-04-2025, 13:15:54", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "display-value-1", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-2", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "DisplayName": "display-value-2", + "TopicName": "topic-name-2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_mappings.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_mappings.validation.json new file mode 100644 index 0000000000000..32d3348a4a4d6 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_mappings.validation.json @@ -0,0 +1,20 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_addition_with_resource": { + "last_validated_date": "2025-04-15T13:05:52+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_deletion_with_resource_remap": { + "last_validated_date": "2025-04-15T13:08:27+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_addition_with_resource": { + "last_validated_date": "2025-04-15T13:07:01+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_deletion_with_resource_remap": { + "last_validated_date": "2025-04-15T13:15:54+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_key_update": { + "last_validated_date": "2025-04-15T13:04:43+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_mappings.py::TestChangeSetMappings::test_mapping_leaf_update": { + "last_validated_date": "2025-04-15T13:03:18+00:00" + } +} From 39368869bae4129b482ea9a1e1e45f8ecedf2a4f Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Wed, 16 Apr 2025 10:26:46 +0100 Subject: [PATCH 058/108] CFn executor v2: provide previous payload correctly (#12511) --- .../services/cloudformation/api_utils.py | 56 +++ .../cloudformation/engine/entities.py | 4 +- .../engine/template_deployer.py | 11 +- .../engine/v2/change_set_model_executor.py | 140 ++++-- .../engine/v2/change_set_model_preproc.py | 31 +- .../cloudformation/resource_provider.py | 4 +- .../services/cloudformation/stores.py | 5 + .../services/cloudformation/v2/entities.py | 201 +++++++++ .../services/cloudformation/v2/provider.py | 419 +++++++++--------- .../resource_providers/aws_ssm_parameter.py | 3 +- .../cloudformation/api/test_changesets.py | 276 +++++++----- .../api/test_changesets.snapshot.json | 22 + .../api/test_changesets.validation.json | 6 + 13 files changed, 797 insertions(+), 381 deletions(-) create mode 100644 localstack-core/localstack/services/cloudformation/v2/entities.py diff --git a/localstack-core/localstack/services/cloudformation/api_utils.py b/localstack-core/localstack/services/cloudformation/api_utils.py index 556435ed699a7..c4172974cec35 100644 --- a/localstack-core/localstack/services/cloudformation/api_utils.py +++ b/localstack-core/localstack/services/cloudformation/api_utils.py @@ -4,6 +4,7 @@ from localstack import config, constants from localstack.aws.connect import connect_to +from localstack.services.cloudformation.engine.validations import ValidationError from localstack.services.s3.utils import ( extract_bucket_name_and_key_from_headers_and_path, normalize_bucket_name, @@ -32,6 +33,61 @@ def prepare_template_body(req_data: dict) -> str | bytes | None: # TODO: mutati return modified_template_body +def extract_template_body(request: dict) -> str: + """ + Given a request payload, fetch the body of the template either from S3 or from the payload itself + """ + if template_body := request.get("TemplateBody"): + if request.get("TemplateURL"): + raise ValidationError( + "Specify exactly one of 'TemplateBody' or 'TemplateUrl'" + ) # TODO: check proper message + + return template_body + + elif template_url := request.get("TemplateURL"): + template_url = convert_s3_to_local_url(template_url) + return get_remote_template_body(template_url) + + else: + raise ValidationError( + "Specify exactly one of 'TemplateBody' or 'TemplateUrl'" + ) # TODO: check proper message + + +def get_remote_template_body(url: str) -> str: + response = run_safe(lambda: safe_requests.get(url, verify=False)) + # check error codes, and code 301 - fixes https://github.com/localstack/localstack/issues/1884 + status_code = 0 if response is None else response.status_code + if 200 <= status_code < 300: + # request was ok + return response.text + elif response is None or status_code == 301 or status_code >= 400: + # check if this is an S3 URL, then get the file directly from there + url = convert_s3_to_local_url(url) + if is_local_service_url(url): + parsed_path = urlparse(url).path.lstrip("/") + parts = parsed_path.partition("/") + client = connect_to().s3 + LOG.debug( + "Download CloudFormation template content from local S3: %s - %s", + parts[0], + parts[2], + ) + result = client.get_object(Bucket=parts[0], Key=parts[2]) + body = to_str(result["Body"].read()) + return body + raise RuntimeError( + "Unable to fetch template body (code %s) from URL %s" % (status_code, url) + ) + else: + raise RuntimeError( + f"Bad status code from fetching template from url '{url}' ({status_code})", + url, + status_code, + ) + + def get_template_body(req_data: dict) -> str: body = req_data.get("TemplateBody") if body: diff --git a/localstack-core/localstack/services/cloudformation/engine/entities.py b/localstack-core/localstack/services/cloudformation/engine/entities.py index 3083d5a2c0363..d9f07f0281e0b 100644 --- a/localstack-core/localstack/services/cloudformation/engine/entities.py +++ b/localstack-core/localstack/services/cloudformation/engine/entities.py @@ -49,7 +49,7 @@ def __init__(self, metadata: dict): self.stack = None -class StackMetadata(TypedDict): +class CreateChangeSetInput(TypedDict): StackName: str Capabilities: list[Capability] ChangeSetName: Optional[str] @@ -83,7 +83,7 @@ def __init__( self, account_id: str, region_name: str, - metadata: Optional[StackMetadata] = None, + metadata: Optional[CreateChangeSetInput] = None, template: Optional[StackTemplate] = None, template_body: Optional[str] = None, ): diff --git a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py index a0ae9c286d61c..16d2fc88f95c1 100644 --- a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py @@ -35,7 +35,6 @@ ProgressEvent, ResourceProviderExecutor, ResourceProviderPayload, - get_resource_type, ) from localstack.services.cloudformation.service_models import ( DependencyNotYetSatisfied, @@ -364,7 +363,7 @@ def _resolve_refs_recursively( ) resource = resources.get(resource_logical_id) - resource_type = get_resource_type(resource) + resource_type = resource["Type"] resolved_getatt = get_attr_from_model_instance( resource, attribute_name, @@ -812,7 +811,7 @@ def _replace(match): resolved = get_attr_from_model_instance( resources[logical_resource_id], attr_name, - get_resource_type(resources[logical_resource_id]), + resources[logical_resource_id]["Type"], logical_resource_id, ) if resolved is None: @@ -1295,7 +1294,7 @@ def apply_change(self, change: ChangeConfig, stack: Stack) -> None: action, logical_resource_id=resource_id ) - resource_provider = executor.try_load_resource_provider(get_resource_type(resource)) + resource_provider = executor.try_load_resource_provider(resource["Type"]) if resource_provider is not None: # add in-progress event resource_status = f"{get_action_name_for_resource_change(action)}_IN_PROGRESS" @@ -1407,7 +1406,7 @@ def delete_stack(self): resource["Properties"] = resource.get( "Properties", clone_safe(resource) ) # TODO: why is there a fallback? - resource["ResourceType"] = get_resource_type(resource) + resource["ResourceType"] = resource["Type"] ordered_resource_ids = list( order_resources( @@ -1438,7 +1437,7 @@ def delete_stack(self): len(resources), resource["ResourceType"], ) - resource_provider = executor.try_load_resource_provider(get_resource_type(resource)) + resource_provider = executor.try_load_resource_provider(resource["Type"]) if resource_provider is not None: event = executor.deploy_loop( resource_provider, resource, resource_provider_payload diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index 60160ef221431..fd1e4fa5b49ef 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -1,12 +1,13 @@ +import copy import logging import uuid -from typing import Any, Final, Optional +from typing import Final, Optional -from localstack.aws.api.cloudformation import ChangeAction +from localstack.aws.api.cloudformation import ChangeAction, StackStatus from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY from localstack.services.cloudformation.engine.v2.change_set_model import ( + NodeParameter, NodeResource, - NodeTemplate, ) from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( ChangeSetModelPreproc, @@ -20,8 +21,8 @@ ProgressEvent, ResourceProviderExecutor, ResourceProviderPayload, - get_resource_type, ) +from localstack.services.cloudformation.v2.entities import ChangeSet LOG = logging.getLogger(__name__) @@ -32,22 +33,27 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc): def __init__( self, - node_template: NodeTemplate, - account_id: str, - region: str, - stack_name: str, - stack_id: str, + change_set: ChangeSet, ): - super().__init__(node_template) - self.account_id = account_id - self.region = region - self.stack_name = stack_name - self.stack_id = stack_id + self.node_template = change_set.update_graph + super().__init__(self.node_template) + self.account_id = change_set.stack.account_id + self.region = change_set.stack.region_name + self.stack = change_set.stack + self.stack_name = self.stack.stack_name + self.stack_id = self.stack.stack_id self.resources = {} + self.resolved_parameters = {} - def execute(self) -> dict: + # TODO: use a structured type for the return value + def execute(self) -> tuple[dict, dict]: self.process() - return self.resources + return self.resources, self.resolved_parameters + + def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta: + delta = super().visit_node_parameter(node_parameter=node_parameter) + self.resolved_parameters[node_parameter.name] = delta.after + return delta def visit_node_resource( self, node_resource: NodeResource @@ -58,9 +64,22 @@ def visit_node_resource( ) return delta - def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> Any: - # TODO: this should be implemented to compute the runtime reference value for node entities. - return super()._reduce_intrinsic_function_ref_value(preproc_value=preproc_value) + def _reduce_intrinsic_function_ref_value(self, preproc_value: PreprocResource | str) -> str: + # TODO: why is this here? + # if preproc_value is None: + # return None + name = preproc_value + if isinstance(preproc_value, PreprocResource): + name = preproc_value.name + resource = self.resources.get(name) + if resource is None: + raise NotImplementedError(f"No resource '{preproc_value.name}' found") + physical_resource_id = resource.get("PhysicalResourceId") + if not physical_resource_id: + raise NotImplementedError( + f"no physical resource id found for resource '{preproc_value.name}'" + ) + return physical_resource_id def _execute_on_resource_change( self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] @@ -70,22 +89,27 @@ def _execute_on_resource_change( # Case: change on same type. if before.resource_type == after.resource_type: # Register a Modified if changed. + # XXX hacky, stick the previous resources' properties into the payload + before_properties = self._merge_before_properties(name, before) + self._execute_resource_action( action=ChangeAction.Modify, logical_resource_id=name, resource_type=before.resource_type, - before_properties=before.properties, + before_properties=before_properties, after_properties=after.properties, ) # Case: type migration. # TODO: Add test to assert that on type change the resources are replaced. else: + # XXX hacky, stick the previous resources' properties into the payload + before_properties = self._merge_before_properties(name, before) # Register a Removed for the previous type. self._execute_resource_action( action=ChangeAction.Remove, logical_resource_id=name, resource_type=before.resource_type, - before_properties=before.properties, + before_properties=before_properties, after_properties=None, ) # Register a Create for the next type. @@ -98,11 +122,15 @@ def _execute_on_resource_change( ) elif before is not None: # Case: removal + # XXX hacky, stick the previous resources' properties into the payload + # XXX hacky, stick the previous resources' properties into the payload + before_properties = self._merge_before_properties(name, before) + self._execute_resource_action( action=ChangeAction.Remove, logical_resource_id=name, resource_type=before.resource_type, - before_properties=before.properties, + before_properties=before_properties, after_properties=None, ) elif after is not None: @@ -115,6 +143,17 @@ def _execute_on_resource_change( after_properties=after.properties, ) + def _merge_before_properties( + self, name: str, preproc_resource: PreprocResource + ) -> PreprocProperties: + if previous_resource_properties := self.stack.resolved_resources.get(name, {}).get( + "Properties" + ): + return PreprocProperties(properties=previous_resource_properties) + + # XXX fall back to returning the input value + return copy.deepcopy(preproc_resource.properties) + def _execute_resource_action( self, action: ChangeAction, @@ -123,11 +162,10 @@ def _execute_resource_action( before_properties: Optional[PreprocProperties], after_properties: Optional[PreprocProperties], ) -> None: + LOG.debug("Executing resource action: %s for resource '%s'", action, logical_resource_id) resource_provider_executor = ResourceProviderExecutor( stack_name=self.stack_name, stack_id=self.stack_id ) - # TODO - resource_type = get_resource_type({"Type": resource_type}) payload = self.create_resource_provider_payload( action=action, logical_resource_id=logical_resource_id, @@ -140,9 +178,22 @@ def _execute_resource_action( extra_resource_properties = {} if resource_provider is not None: # TODO: stack events - event = resource_provider_executor.deploy_loop( - resource_provider, extra_resource_properties, payload - ) + try: + event = resource_provider_executor.deploy_loop( + resource_provider, extra_resource_properties, payload + ) + except Exception as e: + reason = str(e) + LOG.warning( + "Resource provider operation failed: '%s'", + reason, + exc_info=LOG.isEnabledFor(logging.DEBUG), + ) + if self.stack.status == StackStatus.CREATE_IN_PROGRESS: + self.stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) + elif self.stack.status == StackStatus.UPDATE_IN_PROGRESS: + self.stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) + return else: event = ProgressEvent(OperationStatus.SUCCESS, resource_model={}) @@ -156,6 +207,18 @@ def _execute_resource_action( # XXX for legacy delete_stack compatibility self.resources[logical_resource_id]["LogicalResourceId"] = logical_resource_id self.resources[logical_resource_id]["Type"] = resource_type + case OperationStatus.FAILED: + reason = event.message + LOG.warning( + "Resource provider operation failed: '%s'", + reason, + ) + if self.stack.status == StackStatus.CREATE_IN_PROGRESS: + self.stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) + elif self.stack.status == StackStatus.UPDATE_IN_PROGRESS: + self.stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) + else: + raise NotImplementedError(f"Unhandled stack status: '{self.stack.status}'") case any: raise NotImplementedError(f"Event status '{any}' not handled") @@ -174,13 +237,21 @@ def create_resource_provider_payload( "sessionToken": "", } before_properties_value = before_properties.properties if before_properties else None - if action == ChangeAction.Remove: - resource_properties = before_properties_value - previous_resource_properties = None - else: - after_properties_value = after_properties.properties if after_properties else None - resource_properties = after_properties_value - previous_resource_properties = before_properties_value + after_properties_value = after_properties.properties if after_properties else None + + match action: + case ChangeAction.Add: + resource_properties = after_properties_value or {} + previous_resource_properties = None + case ChangeAction.Modify | ChangeAction.Dynamic: + resource_properties = after_properties_value or {} + previous_resource_properties = before_properties_value or {} + case ChangeAction.Remove: + resource_properties = before_properties_value or {} + previous_resource_properties = None + case _: + raise NotImplementedError(f"Action '{action}' not handled") + resource_provider_payload: ResourceProviderPayload = { "awsAccountId": self.account_id, "callbackContext": {}, @@ -193,7 +264,6 @@ def create_resource_provider_payload( "action": str(action), "requestData": { "logicalResourceId": logical_resource_id, - # TODO: assign before and previous according on the action type. "resourceProperties": resource_properties, "previousResourceProperties": previous_resource_properties, "callerCredentials": creds, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index 40c477ce3a545..d716a6116d443 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -173,20 +173,20 @@ def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCon return condition return None - def _resolve_reference(self, logica_id: str) -> PreprocEntityDelta: - node_condition = self._get_node_condition_if_exists(condition_name=logica_id) + def _resolve_reference(self, logical_id: str) -> PreprocEntityDelta: + node_condition = self._get_node_condition_if_exists(condition_name=logical_id) if isinstance(node_condition, NodeCondition): condition_delta = self.visit(node_condition) return condition_delta - node_parameter = self._get_node_parameter_if_exists(parameter_name=logica_id) + node_parameter = self._get_node_parameter_if_exists(parameter_name=logical_id) if isinstance(node_parameter, NodeParameter): parameter_delta = self.visit(node_parameter) return parameter_delta # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments. node_resource = self._get_node_resource_for( - resource_name=logica_id, node_template=self._node_template + resource_name=logical_id, node_template=self._node_template ) resource_delta = self.visit(node_resource) before = resource_delta.before @@ -210,11 +210,11 @@ def _resolve_reference_binding( ) -> PreprocEntityDelta: before = None if before_logical_id is not None: - before_delta = self._resolve_reference(logica_id=before_logical_id) + before_delta = self._resolve_reference(logical_id=before_logical_id) before = before_delta.before after = None if after_logical_id is not None: - after_delta = self._resolve_reference(logica_id=after_logical_id) + after_delta = self._resolve_reference(logical_id=after_logical_id) after = after_delta.after return PreprocEntityDelta(before=before, after=after) @@ -335,7 +335,7 @@ def visit_node_intrinsic_function_fn_if( def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta: condition_name = args[0] - boolean_expression_delta = self._resolve_reference(logica_id=condition_name) + boolean_expression_delta = self._resolve_reference(logical_id=condition_name) return PreprocEntityDelta( before=args[1] if boolean_expression_delta.before else args[2], after=args[1] if boolean_expression_delta.after else args[2], @@ -404,7 +404,7 @@ def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDe delta = self.visit(node_condition.body) return delta - def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> Any: + def _reduce_intrinsic_function_ref_value(self, preproc_value: PreprocResource | str) -> str: if isinstance(preproc_value, PreprocResource): value = preproc_value.name else: @@ -420,16 +420,23 @@ def visit_node_intrinsic_function_ref( before_logical_id = arguments_delta.before before = None if before_logical_id is not None: - before_delta = self._resolve_reference(logica_id=before_logical_id) + before_delta = self._resolve_reference(logical_id=before_logical_id) before_value = before_delta.before - before = self._reduce_intrinsic_function_ref_value(before_value) + if isinstance(before_value, str): + before = before_value + else: + before = self._reduce_intrinsic_function_ref_value(before_value) after_logical_id = arguments_delta.after after = None if after_logical_id is not None: - after_delta = self._resolve_reference(logica_id=after_logical_id) + after_delta = self._resolve_reference(logical_id=after_logical_id) after_value = after_delta.after - after = self._reduce_intrinsic_function_ref_value(after_value) + # TODO: swap isinstance to be a structured type check + if isinstance(after_value, str): + after = after_value + else: + after = self._reduce_intrinsic_function_ref_value(after_value) return PreprocEntityDelta(before=before, after=after) diff --git a/localstack-core/localstack/services/cloudformation/resource_provider.py b/localstack-core/localstack/services/cloudformation/resource_provider.py index 04d7e8f60b4c8..ad590f2257385 100644 --- a/localstack-core/localstack/services/cloudformation/resource_provider.py +++ b/localstack-core/localstack/services/cloudformation/resource_provider.py @@ -444,9 +444,7 @@ def deploy_loop( max_iterations = max(ceil(max_timeout / sleep_time), 2) for current_iteration in range(max_iterations): - resource_type = get_resource_type( - {"Type": raw_payload["resourceType"]} - ) # TODO: simplify signature of get_resource_type to just take the type + resource_type = raw_payload["resourceType"] resource["SpecifiedProperties"] = raw_payload["requestData"]["resourceProperties"] try: diff --git a/localstack-core/localstack/services/cloudformation/stores.py b/localstack-core/localstack/services/cloudformation/stores.py index 11c8fa0cbb879..7191f5491b4e1 100644 --- a/localstack-core/localstack/services/cloudformation/stores.py +++ b/localstack-core/localstack/services/cloudformation/stores.py @@ -3,6 +3,8 @@ from localstack.aws.api.cloudformation import StackStatus from localstack.services.cloudformation.engine.entities import Stack, StackChangeSet, StackSet +from localstack.services.cloudformation.v2.entities import ChangeSet as ChangeSetV2 +from localstack.services.cloudformation.v2.entities import Stack as StackV2 from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute LOG = logging.getLogger(__name__) @@ -11,6 +13,9 @@ class CloudFormationStore(BaseStore): # maps stack ID to stack details stacks: dict[str, Stack] = LocalAttribute(default=dict) + stacks_v2: dict[str, StackV2] = LocalAttribute(default=dict) + + change_sets: dict[str, ChangeSetV2] = LocalAttribute(default=dict) # maps stack set ID to stack set details stack_sets: dict[str, StackSet] = LocalAttribute(default=dict) diff --git a/localstack-core/localstack/services/cloudformation/v2/entities.py b/localstack-core/localstack/services/cloudformation/v2/entities.py new file mode 100644 index 0000000000000..ae9af9ad2ec9f --- /dev/null +++ b/localstack-core/localstack/services/cloudformation/v2/entities.py @@ -0,0 +1,201 @@ +from datetime import datetime, timezone +from typing import TypedDict + +from localstack.aws.api.cloudformation import ( + Changes, + ChangeSetStatus, + ChangeSetType, + CreateChangeSetInput, + DescribeChangeSetOutput, + ExecutionStatus, + Parameter, + StackDriftInformation, + StackDriftStatus, + StackStatus, + StackStatusReason, +) +from localstack.aws.api.cloudformation import ( + Stack as ApiStack, +) +from localstack.services.cloudformation.engine.entities import ( + StackIdentifier, + StackTemplate, +) +from localstack.services.cloudformation.engine.v2.change_set_model import ( + ChangeSetModel, + NodeTemplate, +) +from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( + ChangeSetModelDescriber, +) +from localstack.utils.aws import arns +from localstack.utils.strings import short_uid + + +class ResolvedResource(TypedDict): + Properties: dict + + +class Stack: + stack_name: str + parameters: list[Parameter] + change_set_name: str | None + status: StackStatus + status_reason: StackStatusReason | None + stack_id: str + creation_time: datetime + + # state after deploy + resolved_parameters: dict[str, str] + resolved_resources: dict[str, ResolvedResource] + + def __init__( + self, + account_id: str, + region_name: str, + request_payload: CreateChangeSetInput, + template: StackTemplate | None = None, + template_body: str | None = None, + change_set_ids: list[str] | None = None, + ): + self.account_id = account_id + self.region_name = region_name + self.template = template + self.template_body = template_body + self.status = StackStatus.CREATE_IN_PROGRESS + self.status_reason = None + self.change_set_ids = change_set_ids or [] + self.creation_time = datetime.now(tz=timezone.utc) + + self.stack_name = request_payload["StackName"] + self.change_set_name = request_payload.get("ChangeSetName") + self.parameters = request_payload.get("Parameters", []) + self.stack_id = arns.cloudformation_stack_arn( + self.stack_name, + stack_id=StackIdentifier( + account_id=self.account_id, region=self.region_name, stack_name=self.stack_name + ).generate(tags=request_payload.get("Tags")), + account_id=self.account_id, + region_name=self.region_name, + ) + + # TODO: only kept for v1 compatibility + self.request_payload = request_payload + + # state after deploy + self.resolved_parameters = {} + self.resolved_resources = {} + + def set_stack_status(self, status: StackStatus, reason: StackStatusReason | None = None): + self.status = status + if reason: + self.status_reason = reason + + def describe_details(self) -> ApiStack: + return { + "CreationTime": self.creation_time, + "StackId": self.stack_id, + "StackName": self.stack_name, + "StackStatus": self.status, + "StackStatusReason": self.status_reason, + # fake values + "DisableRollback": False, + "DriftInformation": StackDriftInformation( + StackDriftStatus=StackDriftStatus.NOT_CHECKED + ), + "EnableTerminationProtection": False, + "LastUpdatedTime": self.creation_time, + "RollbackConfiguration": {}, + "Tags": [], + } + + +class ChangeSet: + change_set_name: str + change_set_id: str + change_set_type: ChangeSetType + update_graph: NodeTemplate | None + status: ChangeSetStatus + execution_status: ExecutionStatus + creation_time: datetime + + def __init__( + self, + stack: Stack, + request_payload: CreateChangeSetInput, + template: StackTemplate | None = None, + ): + self.stack = stack + self.template = template + self.status = ChangeSetStatus.CREATE_IN_PROGRESS + self.execution_status = ExecutionStatus.AVAILABLE + self.update_graph = None + self.creation_time = datetime.now(tz=timezone.utc) + + self.change_set_name = request_payload["ChangeSetName"] + self.change_set_type = request_payload.get("ChangeSetType", ChangeSetType.UPDATE) + self.change_set_id = arns.cloudformation_change_set_arn( + self.change_set_name, + change_set_id=short_uid(), + account_id=self.stack.account_id, + region_name=self.stack.region_name, + ) + + def set_change_set_status(self, status: ChangeSetStatus): + self.status = status + + def set_execution_status(self, execution_status: ExecutionStatus): + self.execution_status = execution_status + + @property + def account_id(self) -> str: + return self.stack.account_id + + @property + def region_name(self) -> str: + return self.stack.region_name + + def populate_update_graph( + self, + before_template: dict | None = None, + after_template: dict | None = None, + before_parameters: dict | None = None, + after_parameters: dict | None = None, + ) -> None: + change_set_model = ChangeSetModel( + before_template=before_template, + after_template=after_template, + before_parameters=before_parameters, + after_parameters=after_parameters, + ) + self.update_graph = change_set_model.get_update_model() + + def describe_details(self, include_property_values: bool) -> DescribeChangeSetOutput: + change_set_describer = ChangeSetModelDescriber( + node_template=self.update_graph, + include_property_values=include_property_values, + ) + changes: Changes = change_set_describer.get_changes() + + result = { + "Status": self.status, + "ChangeSetType": self.change_set_type, + "ChangeSetName": self.change_set_name, + "ExecutionStatus": self.execution_status, + "RollbackConfiguration": {}, + "StackId": self.stack.stack_id, + "StackName": self.stack.stack_name, + "StackStatus": self.stack.status, + "CreationTime": self.creation_time, + "LastUpdatedTime": "", + "DisableRollback": "", + "EnableTerminationProtection": "", + "Transform": "", + # TODO: mask no echo + "Parameters": [ + Parameter(ParameterKey=key, ParameterValue=value) + for (key, value) in self.stack.resolved_parameters.items() + ], + "Changes": changes, + } + return result diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 0dfa5ec52b297..beec76b010390 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -1,18 +1,19 @@ -import json import logging -from copy import deepcopy from typing import Any from localstack.aws.api import RequestContext, handler from localstack.aws.api.cloudformation import ( - Changes, ChangeSetNameOrId, ChangeSetNotFoundException, + ChangeSetStatus, ChangeSetType, ClientRequestToken, CreateChangeSetInput, CreateChangeSetOutput, + DeletionMode, DescribeChangeSetOutput, + DescribeStackEventsOutput, + DescribeStacksOutput, DisableRollback, ExecuteChangeSetOutput, ExecutionStatus, @@ -21,22 +22,14 @@ NextToken, Parameter, RetainExceptOnCreate, + RetainResources, + RoleARN, + StackName, StackNameOrId, StackStatus, ) from localstack.services.cloudformation import api_utils -from localstack.services.cloudformation.engine import parameters as param_resolver -from localstack.services.cloudformation.engine import template_deployer, template_preparer -from localstack.services.cloudformation.engine.entities import Stack, StackChangeSet -from localstack.services.cloudformation.engine.parameters import mask_no_echo, strip_parameter_type -from localstack.services.cloudformation.engine.resource_ordering import ( - NoResourceInStack, - order_resources, -) -from localstack.services.cloudformation.engine.template_utils import resolve_stack_conditions -from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( - ChangeSetModelDescriber, -) +from localstack.services.cloudformation.engine import template_preparer from localstack.services.cloudformation.engine.v2.change_set_model_executor import ( ChangeSetModelExecutor, ) @@ -45,32 +38,82 @@ ARN_CHANGESET_REGEX, ARN_STACK_REGEX, CloudformationProvider, - clone_stack_params, ) from localstack.services.cloudformation.stores import ( - find_change_set, - find_stack, + CloudFormationStore, get_cloudformation_store, ) -from localstack.utils.collections import remove_attributes +from localstack.services.cloudformation.v2.entities import ChangeSet, Stack +from localstack.utils.threads import start_worker_thread LOG = logging.getLogger(__name__) +def is_stack_arn(stack_name_or_id: str) -> bool: + return ARN_STACK_REGEX.match(stack_name_or_id) is not None + + +def is_changeset_arn(change_set_name_or_id: str) -> bool: + return ARN_CHANGESET_REGEX.match(change_set_name_or_id) is not None + + +def find_change_set_v2( + state: CloudFormationStore, change_set_name: str, stack_name: str | None = None +) -> ChangeSet | None: + change_set: ChangeSet | None = None + if is_changeset_arn(change_set_name): + change_set = state.change_sets[change_set_name] + else: + if stack_name is not None: + stack: Stack | None = None + if is_stack_arn(stack_name): + stack = state.stacks_v2[stack_name] + else: + for stack_candidate in state.stacks_v2.values(): + # TODO: check for active stacks + if ( + stack_candidate.stack_name == stack_name + and stack.status != StackStatus.DELETE_COMPLETE + ): + stack = stack_candidate + break + + if not stack: + raise NotImplementedError(f"no stack found for change set {change_set_name}") + + for change_set_id in stack.change_set_ids: + change_set_candidate = state.change_sets[change_set_id] + if change_set_candidate.change_set_name == change_set_name: + change_set = change_set_candidate + break + else: + raise NotImplementedError + + return change_set + + class CloudformationProviderV2(CloudformationProvider): @handler("CreateChangeSet", expand=False) def create_change_set( self, context: RequestContext, request: CreateChangeSetInput ) -> CreateChangeSetOutput: + try: + stack_name = request["StackName"] + except KeyError: + # TODO: proper exception + raise ValidationError("StackName must be specified") + try: + change_set_name = request["ChangeSetName"] + except KeyError: + # TODO: proper exception + raise ValidationError("StackName must be specified") + state = get_cloudformation_store(context.account_id, context.region) - req_params = request - change_set_type = req_params.get("ChangeSetType", "UPDATE") - stack_name = req_params.get("StackName") - change_set_name = req_params.get("ChangeSetName") - template_body = req_params.get("TemplateBody") + change_set_type = request.get("ChangeSetType", "UPDATE") + template_body = request.get("TemplateBody") # s3 or secretsmanager url - template_url = req_params.get("TemplateURL") + template_url = request.get("TemplateURL") # validate and resolve template if template_body and template_url: @@ -83,29 +126,19 @@ def create_change_set( "Specify exactly one of 'TemplateBody' or 'TemplateUrl'" ) # TODO: check proper message - api_utils.prepare_template_body( - req_params - ) # TODO: function has too many unclear responsibilities - if not template_body: - template_body = req_params[ - "TemplateBody" - ] # should then have been set by prepare_template_body - template = template_preparer.parse_template(req_params["TemplateBody"]) - - del req_params["TemplateBody"] # TODO: stop mutating req_params - template["StackName"] = stack_name - # TODO: validate with AWS what this is actually doing? - template["ChangeSetName"] = change_set_name + template_body = api_utils.extract_template_body(request) + structured_template = template_preparer.parse_template(template_body) # this is intentionally not in a util yet. Let's first see how the different operations deal with these before generalizing # handle ARN stack_name here (not valid for initial CREATE, since stack doesn't exist yet) - if ARN_STACK_REGEX.match(stack_name): - if not (stack := state.stacks.get(stack_name)): + if is_stack_arn(stack_name): + stack = state.stacks_v2.get(stack_name) + if not stack: raise ValidationError(f"Stack '{stack_name}' does not exist.") else: # stack name specified, so fetch the stack by name stack_candidates: list[Stack] = [ - s for stack_arn, s in state.stacks.items() if s.stack_name == stack_name + s for stack_arn, s in state.stacks_v2.items() if s.stack_name == stack_name ] active_stack_candidates = [ s for s in stack_candidates if self._stack_status_is_active(s.status) @@ -113,23 +146,21 @@ def create_change_set( # on a CREATE an empty Stack should be generated if we didn't find an active one if not active_stack_candidates and change_set_type == ChangeSetType.CREATE: - empty_stack_template = dict(template) - empty_stack_template["Resources"] = {} - req_params_copy = clone_stack_params(req_params) stack = Stack( context.account_id, context.region, - req_params_copy, - empty_stack_template, + request, + structured_template, template_body=template_body, ) - state.stacks[stack.stack_id] = stack - stack.set_stack_status("REVIEW_IN_PROGRESS") + state.stacks_v2[stack.stack_id] = stack else: if not active_stack_candidates: raise ValidationError(f"Stack '{stack_name}' does not exist.") stack = active_stack_candidates[0] + stack.set_stack_status(StackStatus.REVIEW_IN_PROGRESS) + # TODO: test if rollback status is allowed as well if ( change_set_type == ChangeSetType.CREATE @@ -139,14 +170,15 @@ def create_change_set( f"Stack [{stack_name}] already exists and cannot be created again with the changeSet [{change_set_name}]." ) - old_parameters: dict[str, Parameter] = {} + before_parameters: dict[str, Parameter] | None = None match change_set_type: case ChangeSetType.UPDATE: + before_parameters = stack.resolved_parameters # add changeset to existing stack - old_parameters = { - k: mask_no_echo(strip_parameter_type(v)) - for k, v in stack.resolved_parameters.items() - } + # old_parameters = { + # k: mask_no_echo(strip_parameter_type(v)) + # for k, v in stack.resolved_parameters.items() + # } case ChangeSetType.IMPORT: raise NotImplementedError() # TODO: implement importing resources case ChangeSetType.CREATE: @@ -158,130 +190,42 @@ def create_change_set( ) raise ValidationError(msg) - # resolve parameters - new_parameters: dict[str, Parameter] = param_resolver.convert_stack_parameters_to_dict( - request.get("Parameters") - ) - parameter_declarations = param_resolver.extract_stack_parameter_declarations(template) - resolved_parameters = param_resolver.resolve_parameters( - account_id=context.account_id, - region_name=context.region, - parameter_declarations=parameter_declarations, - new_parameters=new_parameters, - old_parameters=old_parameters, - ) - - # TODO: remove this when fixing Stack.resources and transformation order - # currently we need to create a stack with existing resources + parameters so that resolve refs recursively in here will work. - # The correct way to do it would be at a later stage anyway just like a normal intrinsic function - req_params_copy = clone_stack_params(req_params) - temp_stack = Stack(context.account_id, context.region, req_params_copy, template) - temp_stack.set_resolved_parameters(resolved_parameters) - - # TODO: everything below should be async - # apply template transformations - transformed_template = template_preparer.transform_template( - context.account_id, - context.region, - template, - stack_name=temp_stack.stack_name, - resources=temp_stack.resources, - mappings=temp_stack.mappings, - conditions={}, # TODO: we don't have any resolved conditions yet at this point but we need the conditions because of the samtranslator... - resolved_parameters=resolved_parameters, - ) + # TDOO: transformations # TODO: reconsider the way parameters are modelled in the update graph process. # The options might be reduce to using the current style, or passing the extra information # as a metadata object. The choice should be made considering when the extra information # is needed for the update graph building, or only looked up in downstream tasks (metadata). request_parameters = request.get("Parameters", list()) + # TODO: handle parameter defaults and resolution after_parameters: dict[str, Any] = { parameter["ParameterKey"]: parameter["ParameterValue"] for parameter in request_parameters } - before_parameters: dict[str, Any] = { - parameter["ParameterKey"]: parameter["ParameterValue"] - for parameter in old_parameters.values() - } # TODO: update this logic to always pass the clean template object if one exists. The # current issue with relaying on stack.template_original is that this appears to have # its parameters and conditions populated. before_template = None if change_set_type == ChangeSetType.UPDATE: - before_template = json.loads( - stack.template_body - ) # template_original is sometimes invalid - after_template = template + before_template = stack.template + after_template = structured_template # create change set for the stack and apply changes - change_set = StackChangeSet( - context.account_id, - context.region, - stack, - req_params, - transformed_template, - change_set_type=change_set_type, - ) + change_set = ChangeSet(stack, request) + # only set parameters for the changeset, then switch to stack on execute_change_set - change_set.template_body = template_body change_set.populate_update_graph( before_template=before_template, after_template=after_template, before_parameters=before_parameters, after_parameters=after_parameters, ) + change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE) + stack.change_set_id = change_set.change_set_id + state.change_sets[change_set.change_set_id] = change_set - # TODO: move this logic of condition resolution with metadata to the ChangeSetModelPreproc or Executor - raw_conditions = transformed_template.get("Conditions", {}) - resolved_stack_conditions = resolve_stack_conditions( - account_id=context.account_id, - region_name=context.region, - conditions=raw_conditions, - parameters=resolved_parameters, - mappings=temp_stack.mappings, - stack_name=stack_name, - ) - change_set.set_resolved_stack_conditions(resolved_stack_conditions) - change_set.set_resolved_parameters(resolved_parameters) - - # a bit gross but use the template ordering to validate missing resources - try: - order_resources( - transformed_template["Resources"], - resolved_parameters=resolved_parameters, - resolved_conditions=resolved_stack_conditions, - ) - except NoResourceInStack as e: - raise ValidationError(str(e)) from e - - deployer = template_deployer.TemplateDeployer( - context.account_id, context.region, change_set - ) - changes = deployer.construct_changes( - stack, - change_set, - change_set_id=change_set.change_set_id, - append_to_changeset=True, - filter_unchanged_resources=True, - ) - stack.change_sets.append(change_set) - if not changes: - change_set.metadata["Status"] = "FAILED" - change_set.metadata["ExecutionStatus"] = "UNAVAILABLE" - change_set.metadata["StatusReason"] = ( - "The submitted information didn't contain changes. Submit different information to create a change set." - ) - else: - change_set.metadata["Status"] = ( - "CREATE_COMPLETE" # technically for some time this should first be CREATE_PENDING - ) - change_set.metadata["ExecutionStatus"] = ( - "AVAILABLE" # technically for some time this should first be UNAVAILABLE - ) - - return CreateChangeSetOutput(StackId=change_set.stack_id, Id=change_set.change_set_id) + return CreateChangeSetOutput(StackId=stack.stack_id, Id=change_set.change_set_id) @handler("ExecuteChangeSet") def execute_change_set( @@ -294,40 +238,49 @@ def execute_change_set( retain_except_on_create: RetainExceptOnCreate | None = None, **kwargs, ) -> ExecuteChangeSetOutput: - change_set = find_change_set( - context.account_id, - context.region, - change_set_name, - stack_name=stack_name, - active_only=True, - ) + state = get_cloudformation_store(context.account_id, context.region) + + change_set = find_change_set_v2(state, change_set_name, stack_name) if not change_set: raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") - if change_set.metadata.get("ExecutionStatus") != ExecutionStatus.AVAILABLE: + + if change_set.execution_status != ExecutionStatus.AVAILABLE: LOG.debug("Change set %s not in execution status 'AVAILABLE'", change_set_name) raise InvalidChangeSetStatusException( - f"ChangeSet [{change_set.metadata['ChangeSetId']}] cannot be executed in its current status of [{change_set.metadata.get('Status')}]" + f"ChangeSet [{change_set.change_set_id}] cannot be executed in its current status of [{change_set.status}]" ) - stack_name = change_set.stack.stack_name - LOG.debug( - 'Executing change set "%s" for stack "%s" with %s resources ...', - change_set_name, - stack_name, - len(change_set.template_resources), - ) + # LOG.debug( + # 'Executing change set "%s" for stack "%s" with %s resources ...', + # change_set_name, + # stack_name, + # len(change_set.template_resources), + # ) if not change_set.update_graph: raise RuntimeError("Programming error: no update graph found for change set") + change_set.set_execution_status(ExecutionStatus.EXECUTE_IN_PROGRESS) + change_set.stack.set_stack_status( + StackStatus.UPDATE_IN_PROGRESS + if change_set.change_set_type == ChangeSetType.UPDATE + else StackStatus.CREATE_IN_PROGRESS + ) + change_set_executor = ChangeSetModelExecutor( - change_set.update_graph, - account_id=context.account_id, - region=context.region, - stack_name=change_set.stack.stack_name, - stack_id=change_set.stack.stack_id, + change_set, ) - new_resources = change_set_executor.execute() - change_set.stack.set_stack_status(f"{change_set.change_set_type or 'UPDATE'}_COMPLETE") - change_set.stack.resources = new_resources + + def _run(*args): + new_resources, new_parameters = change_set_executor.execute() + new_stack_status = StackStatus.UPDATE_COMPLETE + if change_set.change_set_type == ChangeSetType.CREATE: + new_stack_status = StackStatus.CREATE_COMPLETE + change_set.stack.set_stack_status(new_stack_status) + change_set.set_execution_status(ExecutionStatus.EXECUTE_COMPLETE) + change_set.stack.resolved_resources = new_resources + change_set.stack.resolved_parameters = new_parameters + + start_worker_thread(_run) + return ExecuteChangeSetOutput() @handler("DescribeChangeSet") @@ -342,40 +295,92 @@ def describe_change_set( ) -> DescribeChangeSetOutput: # TODO add support for include_property_values # only relevant if change_set_name isn't an ARN - if not ARN_CHANGESET_REGEX.match(change_set_name): - if not stack_name: - raise ValidationError( - "StackName must be specified if ChangeSetName is not specified as an ARN." - ) - - stack = find_stack(context.account_id, context.region, stack_name) - if not stack: - raise ValidationError(f"Stack [{stack_name}] does not exist") - - change_set = find_change_set( - context.account_id, context.region, change_set_name, stack_name=stack_name - ) + state = get_cloudformation_store(context.account_id, context.region) + change_set = find_change_set_v2(state, change_set_name, stack_name) if not change_set: raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") - change_set_describer = ChangeSetModelDescriber( - node_template=change_set.update_graph, - include_property_values=bool(include_property_values), + result = change_set.describe_details( + include_property_values=include_property_values or False ) - changes: Changes = change_set_describer.get_changes() - - attrs = [ - "ChangeSetType", - "StackStatus", - "LastUpdatedTime", - "DisableRollback", - "EnableTerminationProtection", - "Transform", - ] - result = remove_attributes(deepcopy(change_set.metadata), attrs) - # TODO: replace this patch with a better solution - result["Parameters"] = [ - mask_no_echo(strip_parameter_type(p)) for p in result.get("Parameters", []) - ] - result["Changes"] = changes return result + + @handler("DescribeStacks") + def describe_stacks( + self, + context: RequestContext, + stack_name: StackName = None, + next_token: NextToken = None, + **kwargs, + ) -> DescribeStacksOutput: + state = get_cloudformation_store(context.account_id, context.region) + if stack_name: + if is_stack_arn(stack_name): + stack = state.stacks_v2[stack_name] + else: + stack_candidates = [] + for stack in state.stacks_v2.values(): + if ( + stack.stack_name == stack_name + and stack.status != StackStatus.DELETE_COMPLETE + ): + stack_candidates.append(stack) + if len(stack_candidates) == 0: + raise ValidationError(f"No stack with name {stack_name} found") + elif len(stack_candidates) > 1: + raise RuntimeError("Programing error, duplicate stacks found") + else: + stack = stack_candidates[0] + else: + raise NotImplementedError + + return DescribeStacksOutput(Stacks=[stack.describe_details()]) + + @handler("DescribeStackEvents") + def describe_stack_events( + self, + context: RequestContext, + stack_name: StackName = None, + next_token: NextToken = None, + **kwargs, + ) -> DescribeStackEventsOutput: + return DescribeStackEventsOutput(StackEvents=[]) + + @handler("DeleteStack") + def delete_stack( + self, + context: RequestContext, + stack_name: StackName, + retain_resources: RetainResources = None, + role_arn: RoleARN = None, + client_request_token: ClientRequestToken = None, + deletion_mode: DeletionMode = None, + **kwargs, + ) -> None: + state = get_cloudformation_store(context.account_id, context.region) + if stack_name: + if is_stack_arn(stack_name): + stack = state.stacks_v2[stack_name] + else: + stack_candidates = [] + for stack in state.stacks_v2.values(): + if ( + stack.stack_name == stack_name + and stack.status != StackStatus.DELETE_COMPLETE + ): + stack_candidates.append(stack) + if len(stack_candidates) == 0: + raise ValidationError(f"No stack with name {stack_name} found") + elif len(stack_candidates) > 1: + raise RuntimeError("Programing error, duplicate stacks found") + else: + stack = stack_candidates[0] + else: + raise NotImplementedError + + if not stack: + # aws will silently ignore invalid stack names - we should do the same + return + + # TODO: actually delete + stack.set_stack_status(StackStatus.DELETE_COMPLETE) diff --git a/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py b/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py index 42e834f59ff53..95ea2ecb4d214 100644 --- a/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py +++ b/localstack-core/localstack/services/ssm/resource_providers/aws_ssm_parameter.py @@ -173,7 +173,8 @@ def update( # tag handling new_tags = update_config_props.pop("Tags", {}) - self.update_tags(ssm, model, new_tags) + if new_tags: + self.update_tags(ssm, model, new_tags) ssm.put_parameter(Overwrite=True, Tags=[], **update_config_props) diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index 3ccf088f6bbe5..4666e22c6b263 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -104,12 +104,18 @@ def test_simple_update_two_resources( res.destroy() - @markers.aws.needs_fixing - @pytest.mark.skip(reason="WIP") - def test_deleting_resource(self, aws_client: ServiceLevelClientFactory, deploy_cfn_template): + @markers.aws.validated + # TODO: the error response is incorrect, however the test is otherwise validated and raises + # an error because the SSM parameter has been deleted (removed from the stack). + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message", "$..message"]) + @pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Test fails with the old engine" + ) + def test_deleting_resource( + self, aws_client: ServiceLevelClientFactory, deploy_cfn_template, snapshot + ): parameter_name = "my-parameter" value1 = "foo" - stack_name = f"stack-{short_uid()}" t1 = { "Resources": { @@ -131,20 +137,18 @@ def test_deleting_resource(self, aws_client: ServiceLevelClientFactory, deploy_c }, } - res = deploy_cfn_template(stack_name=stack_name, template=json.dumps(t1), is_update=False) + stack = deploy_cfn_template(template=json.dumps(t1)) found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] assert found_value == value1 t2 = copy.deepcopy(t1) del t2["Resources"]["MyParameter2"] - deploy_cfn_template(stack_name=stack_name, template=json.dumps(t2), is_update=True) + deploy_cfn_template(stack_name=stack.stack_name, template=json.dumps(t2), is_update=True) with pytest.raises(ClientError) as exc_info: - aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] - - assert f"Parameter {parameter_name} not found" in str(exc_info.value) + aws_client.ssm.get_parameter(Name=parameter_name) - res.destroy() + snapshot.match("get-parameter-error", exc_info.value.response) @markers.aws.validated @@ -1268,7 +1272,6 @@ def test_direct_update( capture_update_process(snapshot, t1, t2) @markers.aws.validated - @pytest.mark.skip("Deployment fails, as executor is WIP") def test_dynamic_update( self, snapshot, @@ -1331,10 +1334,6 @@ def test_dynamic_update( capture_update_process(snapshot, t1, t2) @markers.aws.validated - @pytest.mark.skip( - "Template deployment appears to fail on v2 due to unresolved resource dependencies; " - "this should be addressed in the development of the v2 engine executor." - ) def test_parameter_changes( self, snapshot, @@ -1383,7 +1382,6 @@ def test_parameter_changes( capture_update_process(snapshot, t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) @markers.aws.validated - @pytest.mark.skip("Deployment fails, as executor is WIP") def test_mappings_with_static_fields( self, snapshot, @@ -1470,10 +1468,6 @@ def test_mappings_with_static_fields( capture_update_process(snapshot, t1, t2) @markers.aws.validated - @pytest.mark.skip( - "Template deployment appears to fail on v2 due to unresolved resource dependencies; " - "this should be addressed in the development of the v2 engine executor." - ) def test_mappings_with_parameter_lookup( self, snapshot, @@ -1642,7 +1636,7 @@ def test_unrelated_changes_update_propagation( @markers.aws.validated @pytest.mark.skip( - "Deployment fails however this appears to be unrelated from the update graph building and describe" + "Deployment now succeeds but our describer incorrectly does not assign a change for Parameter2" ) def test_unrelated_changes_requires_replacement( self, @@ -1701,108 +1695,116 @@ def test_unrelated_changes_requires_replacement( capture_update_process(snapshot, t1, t2) @markers.aws.validated - @pytest.mark.skip("Executor is WIP") + # @pytest.mark.skip("Executor is WIP") @pytest.mark.parametrize( "template", [ - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - } - }, - }, - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "param-name", - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, - }, - }, - }, - }, - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, - }, - }, - }, - }, - { - "Parameters": { - "ParameterValue": { - "Type": "String", - "Default": "value-1", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { + pytest.param( + { + "Parameters": { + "ParameterValue": { "Type": "String", - "Value": "first", }, }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + } }, }, - }, - ], - ids=[ - "change_dynamic", - "change_unrelated_property", - "change_unrelated_property_not_create_only", - "change_parameter_for_condition_create_resource", + id="change_dynamic", + ), + # pytest.param( + # { + # "Parameters": { + # "ParameterValue": { + # "Type": "String", + # }, + # }, + # "Resources": { + # "Parameter1": { + # "Type": "AWS::SSM::Parameter", + # "Properties": { + # "Name": "param-name", + # "Type": "String", + # "Value": {"Ref": "ParameterValue"}, + # }, + # }, + # "Parameter2": { + # "Type": "AWS::SSM::Parameter", + # "Properties": { + # "Type": "String", + # "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, + # }, + # }, + # }, + # }, + # id="change_unrelated_property", + # ), + # pytest.param( + # { + # "Parameters": { + # "ParameterValue": { + # "Type": "String", + # }, + # }, + # "Resources": { + # "Parameter1": { + # "Type": "AWS::SSM::Parameter", + # "Properties": { + # "Type": "String", + # "Value": {"Ref": "ParameterValue"}, + # }, + # }, + # "Parameter2": { + # "Type": "AWS::SSM::Parameter", + # "Properties": { + # "Type": "String", + # "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, + # }, + # }, + # }, + # }, + # id="change_unrelated_property_not_create_only", + # ), + # pytest.param( + # { + # "Parameters": { + # "ParameterValue": { + # "Type": "String", + # "Default": "value-1", + # "AllowedValues": ["value-1", "value-2"], + # } + # }, + # "Conditions": { + # "ShouldCreateParameter": { + # "Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"] + # } + # }, + # "Resources": { + # "SSMParameter1": { + # "Type": "AWS::SSM::Parameter", + # "Properties": { + # "Type": "String", + # "Value": "first", + # }, + # }, + # "SSMParameter2": { + # "Type": "AWS::SSM::Parameter", + # "Condition": "ShouldCreateParameter", + # "Properties": { + # "Type": "String", + # "Value": "first", + # }, + # }, + # }, + # }, + # id="change_parameter_for_condition_create_resource", + # ), ], ) def test_base_dynamic_parameter_scenarios( @@ -1819,6 +1821,50 @@ def test_base_dynamic_parameter_scenarios( {"ParameterValue": "value-2"}, ) + @markers.aws.validated + def test_execute_with_ref(self, snapshot, aws_client, deploy_cfn_template): + name1 = f"param-1-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(name1, "")) + name2 = f"param-2-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(name2, "")) + value = "my-value" + param2_name = f"output-param-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(param2_name, "")) + + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Type": "String", + "Value": value, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": param2_name, + "Type": "String", + "Value": {"Ref": "Parameter1"}, + }, + }, + } + } + t2 = copy.deepcopy(t1) + t2["Resources"]["Parameter1"]["Properties"]["Name"] = name2 + + stack = deploy_cfn_template(template=json.dumps(t1)) + stack_id = stack.stack_id + + before_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] + snapshot.match("before-value", before_value) + + deploy_cfn_template(stack_name=stack_id, template=json.dumps(t2), is_update=True) + + after_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] + snapshot.match("after-value", after_value) + @markers.aws.validated @pytest.mark.skip("Executor is WIP") @pytest.mark.parametrize( diff --git a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json index 0020e238e7865..ec3e3ec58f808 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json @@ -7261,5 +7261,27 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_execute_with_ref": { + "recorded-date": "11-04-2025, 14:34:15", + "recorded-content": { + "before-value": "", + "after-value": "" + } + }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_deleting_resource": { + "recorded-date": "15-04-2025, 15:07:18", + "recorded-content": { + "get-parameter-error": { + "Error": { + "Code": "ParameterNotFound", + "Message": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/cloudformation/api/test_changesets.validation.json b/tests/aws/services/cloudformation/api/test_changesets.validation.json index caa6ed4e295d0..3c3b7ffa3c6c3 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.validation.json +++ b/tests/aws/services/cloudformation/api/test_changesets.validation.json @@ -23,6 +23,9 @@ "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { "last_validated_date": "2025-04-01T12:30:53+00:00" }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_execute_with_ref": { + "last_validated_date": "2025-04-11T14:34:09+00:00" + }, "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { "last_validated_date": "2025-04-01T13:31:33+00:00" }, @@ -38,6 +41,9 @@ "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { "last_validated_date": "2025-04-01T16:40:03+00:00" }, + "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_deleting_resource": { + "last_validated_date": "2025-04-15T15:07:18+00:00" + }, "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_simple_update_two_resources": { "last_validated_date": "2025-04-02T10:05:26+00:00" }, From ef8845dae56c4c5a20f11e79eb22689b57f734ed Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 16 Apr 2025 12:17:32 -0300 Subject: [PATCH 059/108] [AWS][Transcribe] Adding fix for validating Audio length (#12450) --- .../services/transcribe/provider.py | 10 ++++++ .../services/transcribe/test_transcribe.py | 35 +++++++++++++++++++ .../transcribe/test_transcribe.snapshot.json | 27 ++++++++++++++ .../test_transcribe.validation.json | 3 ++ 4 files changed, 75 insertions(+) diff --git a/localstack-core/localstack/services/transcribe/provider.py b/localstack-core/localstack/services/transcribe/provider.py index 29fbdf6a552e1..79a9cea6d50b2 100644 --- a/localstack-core/localstack/services/transcribe/provider.py +++ b/localstack-core/localstack/services/transcribe/provider.py @@ -43,6 +43,11 @@ from localstack.utils.run import run from localstack.utils.threads import start_thread +# Amazon Transcribe service calls are limited to four hours (or 2 GB) per API call for our batch service. +# The streaming service can accommodate open connections up to four hours long. +# See https://aws.amazon.com/transcribe/faqs/ +MAX_AUDIO_DURATION_SECONDS = 60 * 60 * 4 + LOG = logging.getLogger(__name__) VOSK_MODELS_URL = f"{HUGGING_FACE_ENDPOINT}/vosk-models/resolve/main/" @@ -305,6 +310,11 @@ def _run_transcription_job(self, args: Tuple[TranscribeStore, str]): format = ffprobe_output["format"]["format_name"] LOG.debug("Media format detected as: %s", format) job["MediaFormat"] = SUPPORTED_FORMAT_NAMES[format] + duration = ffprobe_output["format"]["duration"] + + if float(duration) >= MAX_AUDIO_DURATION_SECONDS: + failure_reason = "Invalid file size: file size too large. Maximum audio duration is 4.000000 hours.Check the length of the file and try your request again." + raise RuntimeError() # Determine the sample rate of input audio if possible for stream in ffprobe_output["streams"]: diff --git a/tests/aws/services/transcribe/test_transcribe.py b/tests/aws/services/transcribe/test_transcribe.py index 572b1b0a4c0b1..e3235ade6ce8b 100644 --- a/tests/aws/services/transcribe/test_transcribe.py +++ b/tests/aws/services/transcribe/test_transcribe.py @@ -1,5 +1,6 @@ import logging import os +import tempfile import threading import time from urllib.parse import urlparse @@ -16,6 +17,7 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.files import new_tmp_file +from localstack.utils.run import run from localstack.utils.strings import short_uid, to_str from localstack.utils.sync import poll_condition, retry from localstack.utils.threads import start_worker_thread @@ -439,3 +441,36 @@ def test_transcribe_error_speaker_labels(self, transcribe_create_job, aws_client with pytest.raises(ParamValidationError) as e: transcribe_create_job(audio_file=file_path, params=settings) snapshot.match("err_speaker_labels_diarization", e.value) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..TranscriptionJob..Settings", + "$..TranscriptionJob..Transcript", + "$..TranscriptionJob..MediaFormat", + ] + ) + def test_transcribe_error_invalid_length(self, transcribe_create_job, aws_client, snapshot): + ffmpeg_bin = ffmpeg_package.get_installer().get_ffmpeg_path() + media_file = os.path.join(tempfile.gettempdir(), "audio_4h.mp3") + + run( + f"{ffmpeg_bin} -f lavfi -i anullsrc=r=44100:cl=mono -t 14400 -q:a 9 -acodec libmp3lame {media_file}" + ) + job_name = transcribe_create_job(audio_file=media_file) + + def _is_transcription_done(): + transcription_status = aws_client.transcribe.get_transcription_job( + TranscriptionJobName=job_name + ) + return transcription_status["TranscriptionJob"]["TranscriptionJobStatus"] == "FAILED" + + # empirically it takes around + # <5sec for a vosk transcription + # ~100sec for an AWS transcription -> adjust timeout accordingly + assert poll_condition(_is_transcription_done, timeout=100), ( + f"could not finish transcription job: {job_name} in time" + ) + + job = aws_client.transcribe.get_transcription_job(TranscriptionJobName=job_name) + snapshot.match("TranscribeErrorInvalidLength", job) diff --git a/tests/aws/services/transcribe/test_transcribe.snapshot.json b/tests/aws/services/transcribe/test_transcribe.snapshot.json index 22de82a62c258..8a879cea33edd 100644 --- a/tests/aws/services/transcribe/test_transcribe.snapshot.json +++ b/tests/aws/services/transcribe/test_transcribe.snapshot.json @@ -893,5 +893,32 @@ "recorded-content": { "err_speaker_labels_diarization": "Parameter validation failed:\nInvalid value for parameter Settings.MaxSpeakerLabels, value: 1, valid min value: 2" } + }, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_error_invalid_length": { + "recorded-date": "12-04-2025, 16:02:39", + "recorded-content": { + "TranscribeErrorInvalidLength": { + "TranscriptionJob": { + "CreationTime": "datetime", + "FailureReason": "Invalid file size: file size too large. Maximum audio duration is 4.000000 hours.Check the length of the file and try your request again.", + "LanguageCode": "en-GB", + "Media": { + "MediaFileUri": "s3://test-clip.wav" + }, + "Settings": { + "ChannelIdentification": false, + "ShowAlternatives": false + }, + "StartTime": "datetime", + "Transcript": {}, + "TranscriptionJobName": "", + "TranscriptionJobStatus": "FAILED" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/transcribe/test_transcribe.validation.json b/tests/aws/services/transcribe/test_transcribe.validation.json index d03cfbea1cc28..d013e9960e42d 100644 --- a/tests/aws/services/transcribe/test_transcribe.validation.json +++ b/tests/aws/services/transcribe/test_transcribe.validation.json @@ -11,6 +11,9 @@ "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_list_transcription_jobs": { "last_validated_date": "2023-10-06T15:11:25+00:00" }, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_error_invalid_length": { + "last_validated_date": "2025-04-12T16:02:38+00:00" + }, "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_error_speaker_labels": { "last_validated_date": "2025-03-19T15:42:06+00:00" }, From 2069add1c14cae35a5d0a272c529826d97a42112 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:58:15 +0200 Subject: [PATCH 060/108] CloudFormation Engine V2: Improve delta computation of properties, conditional resolution, and physical resources ref (#12533) --- .../engine/v2/change_set_model.py | 5 +- .../engine/v2/change_set_model_describer.py | 7 +- .../engine/v2/change_set_model_executor.py | 113 +++++----- .../engine/v2/change_set_model_preproc.py | 33 ++- .../cloudformation/api/test_changesets.py | 193 +++++++++--------- 5 files changed, 183 insertions(+), 168 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 3d65187172611..93ef9b32aae41 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -769,7 +769,10 @@ def _visit_resource( before_properties=before_properties, after_properties=after_properties, ) - change_type = change_type.for_child(properties.change_type) + if properties.properties: + # Properties were defined in the before or after template, thus must play a role + # in affecting the change type of this resource. + change_type = change_type.for_child(properties.change_type) node_resource = NodeResource( scope=scope, change_type=change_type, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index 35115f4ceb05c..49a07a2b426e8 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -82,10 +82,6 @@ def _register_resource_change( before_properties: Optional[PreprocProperties], after_properties: Optional[PreprocProperties], ) -> None: - # unchanged: nothing to do. - if before_properties == after_properties: - return - action = cfn_api.ChangeAction.Modify if before_properties is None: action = cfn_api.ChangeAction.Add @@ -111,6 +107,9 @@ def _register_resource_change( def _describe_resource_change( self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] ) -> None: + if before == after: + # unchanged: nothing to do. + return if before is not None and after is not None: # Case: change on same type. if before.resource_type == after.resource_type: diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index fd1e4fa5b49ef..6bcb424e194b6 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -1,7 +1,7 @@ import copy import logging import uuid -from typing import Final, Optional +from typing import Any, Final, Optional from localstack.aws.api.cloudformation import ChangeAction, StackStatus from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY @@ -28,22 +28,16 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc): - account_id: Final[str] - region: Final[str] + change_set: Final[ChangeSet] + # TODO: add typing. + resources: Final[dict] + resolved_parameters: Final[dict] - def __init__( - self, - change_set: ChangeSet, - ): - self.node_template = change_set.update_graph - super().__init__(self.node_template) - self.account_id = change_set.stack.account_id - self.region = change_set.stack.region_name - self.stack = change_set.stack - self.stack_name = self.stack.stack_name - self.stack_id = self.stack.stack_id - self.resources = {} - self.resolved_parameters = {} + def __init__(self, change_set: ChangeSet): + super().__init__(node_template=change_set.update_graph) + self.change_set = change_set + self.resources = dict() + self.resolved_parameters = dict() # TODO: use a structured type for the return value def execute(self) -> tuple[dict, dict]: @@ -64,26 +58,42 @@ def visit_node_resource( ) return delta - def _reduce_intrinsic_function_ref_value(self, preproc_value: PreprocResource | str) -> str: - # TODO: why is this here? - # if preproc_value is None: - # return None - name = preproc_value - if isinstance(preproc_value, PreprocResource): - name = preproc_value.name - resource = self.resources.get(name) - if resource is None: - raise NotImplementedError(f"No resource '{preproc_value.name}' found") - physical_resource_id = resource.get("PhysicalResourceId") - if not physical_resource_id: - raise NotImplementedError( - f"no physical resource id found for resource '{preproc_value.name}'" - ) - return physical_resource_id + def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> PreprocEntityDelta: + if not isinstance(preproc_value, PreprocResource): + return super()._reduce_intrinsic_function_ref_value(preproc_value=preproc_value) + + logical_id = preproc_value.name + + def _get_physical_id_of_resolved_resource(resolved_resource: dict) -> str: + physical_resource_id = resolved_resource.get("PhysicalResourceId") + if not isinstance(physical_resource_id, str): + raise RuntimeError( + f"No physical resource id found for resource '{logical_id}' during ChangeSet execution" + ) + return physical_resource_id + + before_resolved_resources = self.change_set.stack.resolved_resources + after_resolved_resources = self.resources + + before_physical_id = None + if logical_id in before_resolved_resources: + before_resolved_resource = before_resolved_resources[logical_id] + before_physical_id = _get_physical_id_of_resolved_resource(before_resolved_resource) + after_physical_id = None + if logical_id in after_resolved_resources: + after_resolved_resource = after_resolved_resources[logical_id] + after_physical_id = _get_physical_id_of_resolved_resource(after_resolved_resource) + + if before_physical_id is None and after_physical_id is None: + raise RuntimeError(f"No resource '{logical_id}' found during ChangeSet execution") + return PreprocEntityDelta(before=before_physical_id, after=after_physical_id) def _execute_on_resource_change( self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] ) -> None: + if before == after: + # unchanged: nothing to do. + return # TODO: this logic is a POC and should be revised. if before is not None and after is not None: # Case: change on same type. @@ -146,9 +156,9 @@ def _execute_on_resource_change( def _merge_before_properties( self, name: str, preproc_resource: PreprocResource ) -> PreprocProperties: - if previous_resource_properties := self.stack.resolved_resources.get(name, {}).get( - "Properties" - ): + if previous_resource_properties := self.change_set.stack.resolved_resources.get( + name, {} + ).get("Properties"): return PreprocProperties(properties=previous_resource_properties) # XXX fall back to returning the input value @@ -164,7 +174,7 @@ def _execute_resource_action( ) -> None: LOG.debug("Executing resource action: %s for resource '%s'", action, logical_resource_id) resource_provider_executor = ResourceProviderExecutor( - stack_name=self.stack_name, stack_id=self.stack_id + stack_name=self.change_set.stack.stack_name, stack_id=self.change_set.stack.stack_id ) payload = self.create_resource_provider_payload( action=action, @@ -189,10 +199,12 @@ def _execute_resource_action( reason, exc_info=LOG.isEnabledFor(logging.DEBUG), ) - if self.stack.status == StackStatus.CREATE_IN_PROGRESS: - self.stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) - elif self.stack.status == StackStatus.UPDATE_IN_PROGRESS: - self.stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) + stack = self.change_set.stack + stack_status = stack.status + if stack_status == StackStatus.CREATE_IN_PROGRESS: + stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) + elif stack_status == StackStatus.UPDATE_IN_PROGRESS: + stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) return else: event = ProgressEvent(OperationStatus.SUCCESS, resource_model={}) @@ -213,12 +225,15 @@ def _execute_resource_action( "Resource provider operation failed: '%s'", reason, ) - if self.stack.status == StackStatus.CREATE_IN_PROGRESS: - self.stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) - elif self.stack.status == StackStatus.UPDATE_IN_PROGRESS: - self.stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) + # TODO: duplication + stack = self.change_set.stack + stack_status = stack.status + if stack_status == StackStatus.CREATE_IN_PROGRESS: + stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) + elif stack_status == StackStatus.UPDATE_IN_PROGRESS: + stack.set_stack_status(StackStatus.UPDATE_FAILED, reason=reason) else: - raise NotImplementedError(f"Unhandled stack status: '{self.stack.status}'") + raise NotImplementedError(f"Unhandled stack status: '{stack.status}'") case any: raise NotImplementedError(f"Event status '{any}' not handled") @@ -232,7 +247,7 @@ def create_resource_provider_payload( ) -> Optional[ResourceProviderPayload]: # FIXME: use proper credentials creds: Credentials = { - "accessKeyId": self.account_id, + "accessKeyId": self.change_set.stack.account_id, "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY, "sessionToken": "", } @@ -253,14 +268,14 @@ def create_resource_provider_payload( raise NotImplementedError(f"Action '{action}' not handled") resource_provider_payload: ResourceProviderPayload = { - "awsAccountId": self.account_id, + "awsAccountId": self.change_set.stack.account_id, "callbackContext": {}, - "stackId": self.stack_name, + "stackId": self.change_set.stack.stack_name, "resourceType": resource_type, "resourceTypeVersion": "000000", # TODO: not actually a UUID "bearerToken": str(uuid.uuid4()), - "region": self.region, + "region": self.change_set.stack.region_name, "action": str(action), "requestData": { "logicalResourceId": logical_resource_id, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index d716a6116d443..025aeadc52c18 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -78,13 +78,20 @@ def __init__( self.resource_type = resource_type self.properties = properties + @staticmethod + def _compare_conditions(c1: bool, c2: bool): + # The lack of condition equates to a true condition. + c1 = c1 if isinstance(c1, bool) else True + c2 = c2 if isinstance(c2, bool) else True + return c1 == c2 + def __eq__(self, other): if not isinstance(other, PreprocResource): return False return all( [ self.name == other.name, - self.condition == other.condition, + self._compare_conditions(self.condition, other.condition), self.resource_type == other.resource_type, self.properties == other.properties, ] @@ -404,12 +411,12 @@ def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDe delta = self.visit(node_condition.body) return delta - def _reduce_intrinsic_function_ref_value(self, preproc_value: PreprocResource | str) -> str: + def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> PreprocEntityDelta: if isinstance(preproc_value, PreprocResource): value = preproc_value.name else: value = preproc_value - return value + return PreprocEntityDelta(value, value) def visit_node_intrinsic_function_ref( self, node_intrinsic_function: NodeIntrinsicFunction @@ -422,10 +429,8 @@ def visit_node_intrinsic_function_ref( if before_logical_id is not None: before_delta = self._resolve_reference(logical_id=before_logical_id) before_value = before_delta.before - if isinstance(before_value, str): - before = before_value - else: - before = self._reduce_intrinsic_function_ref_value(before_value) + before_ref_delta = self._reduce_intrinsic_function_ref_value(before_value) + before = before_ref_delta.before after_logical_id = arguments_delta.after after = None @@ -433,10 +438,8 @@ def visit_node_intrinsic_function_ref( after_delta = self._resolve_reference(logical_id=after_logical_id) after_value = after_delta.after # TODO: swap isinstance to be a structured type check - if isinstance(after_value, str): - after = after_value - else: - after = self._reduce_intrinsic_function_ref_value(after_value) + after_ref_delta = self._reduce_intrinsic_function_ref_value(after_value) + after = after_ref_delta.after return PreprocEntityDelta(before=before, after=after) @@ -466,12 +469,8 @@ def visit_node_properties( before_bindings[property_name] = delta.before if node_property.change_type != ChangeType.REMOVED: after_bindings[property_name] = delta.after - before = None - if before_bindings: - before = PreprocProperties(properties=before_bindings) - after = None - if after_bindings: - after = PreprocProperties(properties=after_bindings) + before = PreprocProperties(properties=before_bindings) + after = PreprocProperties(properties=after_bindings) return PreprocEntityDelta(before=before, after=after) def _resolve_resource_condition_reference(self, reference: TerminalValue) -> PreprocEntityDelta: diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index 4666e22c6b263..56af7886cb019 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -1526,10 +1526,6 @@ def test_mappings_with_parameter_lookup( capture_update_process(snapshot, t1, t1, p1={"TopicName": "key1"}, p2={"TopicName": "key2"}) @markers.aws.validated - @pytest.mark.skip( - "Template deployment appears to fail on v2 due to unresolved resource dependencies; " - "this should be addressed in the development of the v2 engine executor." - ) def test_conditions( self, snapshot, @@ -1695,7 +1691,6 @@ def test_unrelated_changes_requires_replacement( capture_update_process(snapshot, t1, t2) @markers.aws.validated - # @pytest.mark.skip("Executor is WIP") @pytest.mark.parametrize( "template", [ @@ -1718,101 +1713,106 @@ def test_unrelated_changes_requires_replacement( }, id="change_dynamic", ), - # pytest.param( - # { - # "Parameters": { - # "ParameterValue": { - # "Type": "String", - # }, - # }, - # "Resources": { - # "Parameter1": { - # "Type": "AWS::SSM::Parameter", - # "Properties": { - # "Name": "param-name", - # "Type": "String", - # "Value": {"Ref": "ParameterValue"}, - # }, - # }, - # "Parameter2": { - # "Type": "AWS::SSM::Parameter", - # "Properties": { - # "Type": "String", - # "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, - # }, - # }, - # }, - # }, - # id="change_unrelated_property", - # ), - # pytest.param( - # { - # "Parameters": { - # "ParameterValue": { - # "Type": "String", - # }, - # }, - # "Resources": { - # "Parameter1": { - # "Type": "AWS::SSM::Parameter", - # "Properties": { - # "Type": "String", - # "Value": {"Ref": "ParameterValue"}, - # }, - # }, - # "Parameter2": { - # "Type": "AWS::SSM::Parameter", - # "Properties": { - # "Type": "String", - # "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, - # }, - # }, - # }, - # }, - # id="change_unrelated_property_not_create_only", - # ), - # pytest.param( - # { - # "Parameters": { - # "ParameterValue": { - # "Type": "String", - # "Default": "value-1", - # "AllowedValues": ["value-1", "value-2"], - # } - # }, - # "Conditions": { - # "ShouldCreateParameter": { - # "Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"] - # } - # }, - # "Resources": { - # "SSMParameter1": { - # "Type": "AWS::SSM::Parameter", - # "Properties": { - # "Type": "String", - # "Value": "first", - # }, - # }, - # "SSMParameter2": { - # "Type": "AWS::SSM::Parameter", - # "Condition": "ShouldCreateParameter", - # "Properties": { - # "Type": "String", - # "Value": "first", - # }, - # }, - # }, - # }, - # id="change_parameter_for_condition_create_resource", - # ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "param-name", + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, + }, + }, + }, + }, + id="change_unrelated_property", + ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, + }, + }, + }, + }, + id="change_unrelated_property_not_create_only", + ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + "Default": "value-1", + "AllowedValues": ["value-1", "value-2"], + } + }, + "Conditions": { + "ShouldCreateParameter": { + "Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"] + } + }, + "Resources": { + "SSMParameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + "SSMParameter2": { + "Type": "AWS::SSM::Parameter", + "Condition": "ShouldCreateParameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + }, + }, + id="change_parameter_for_condition_create_resource", + ), ], ) def test_base_dynamic_parameter_scenarios( - self, - snapshot, - capture_update_process, - template, + self, snapshot, capture_update_process, template, request ): + if request.node.callspec.id in { + "change_unrelated_property", + "change_unrelated_property_not_create_only", + }: + pytest.skip( + reason="AWS appears to incorrectly mark the dependent resource as needing update when describe " + "changeset is invoked without the inclusion of property values." + ) capture_update_process( snapshot, template, @@ -1866,7 +1866,6 @@ def test_execute_with_ref(self, snapshot, aws_client, deploy_cfn_template): snapshot.match("after-value", after_value) @markers.aws.validated - @pytest.mark.skip("Executor is WIP") @pytest.mark.parametrize( "template_1, template_2", [ From 9e84b8d2776869ffd8fff7f9919cffe90838ead2 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Thu, 17 Apr 2025 13:50:48 +0530 Subject: [PATCH 061/108] Bump moto-ext to 5.1.3.post1 (#12499) --- pyproject.toml | 2 +- requirements-dev.txt | 2 +- requirements-runtime.txt | 2 +- requirements-test.txt | 2 +- requirements-typehint.txt | 2 +- tests/aws/services/cloudformation/resources/test_ec2.py | 2 ++ .../services/cloudformation/resources/test_ec2.snapshot.json | 3 ++- .../services/cloudformation/resources/test_ec2.validation.json | 2 +- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ac9fb745ce19..cc1a5be68b614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.1.1.post2", + "moto-ext[all]==5.1.3.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index ab2f7558364da..82d230cb8e48a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -250,7 +250,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post2 +moto-ext==5.1.3.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-runtime.txt b/requirements-runtime.txt index ff7a9a6826073..378165d67c158 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -188,7 +188,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post2 +moto-ext==5.1.3.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy diff --git a/requirements-test.txt b/requirements-test.txt index 7e38839b9f68f..67715c62c9c7d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -234,7 +234,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post2 +moto-ext==5.1.3.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-typehint.txt b/requirements-typehint.txt index a2e55fdef1249..c919ac8a799af 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -254,7 +254,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.6.0 # via openapi-core -moto-ext==5.1.1.post2 +moto-ext==5.1.3.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/tests/aws/services/cloudformation/resources/test_ec2.py b/tests/aws/services/cloudformation/resources/test_ec2.py index fd02a304130ce..84928dc37c21b 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.py +++ b/tests/aws/services/cloudformation/resources/test_ec2.py @@ -155,6 +155,8 @@ def test_dhcp_options(aws_client, deploy_cfn_template, snapshot): "$..Tags", "$..Options.AssociationDefaultRouteTableId", "$..Options.PropagationDefaultRouteTableId", + "$..Options.TransitGatewayCidrBlocks", # an empty list returned by Moto but not by AWS + "$..Options.SecurityGroupReferencingSupport", # not supported by Moto ] ) def test_transit_gateway_attachment(deploy_cfn_template, aws_client, snapshot): diff --git a/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json b/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json index 024a531d45896..0f42548858457 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json @@ -91,7 +91,7 @@ } }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_transit_gateway_attachment": { - "recorded-date": "28-03-2024, 06:48:11", + "recorded-date": "08-04-2025, 10:51:02", "recorded-content": { "attachment": { "Association": { @@ -125,6 +125,7 @@ "DnsSupport": "enable", "MulticastSupport": "disable", "PropagationDefaultRouteTableId": "", + "SecurityGroupReferencingSupport": "disable", "VpnEcmpSupport": "enable" }, "OwnerId": "111111111111", diff --git a/tests/aws/services/cloudformation/resources/test_ec2.validation.json b/tests/aws/services/cloudformation/resources/test_ec2.validation.json index e9b8da44359c4..6eb9f2caf3324 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.validation.json +++ b/tests/aws/services/cloudformation/resources/test_ec2.validation.json @@ -24,7 +24,7 @@ "last_validated_date": "2024-07-01T20:10:52+00:00" }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_transit_gateway_attachment": { - "last_validated_date": "2024-03-28T06:48:11+00:00" + "last_validated_date": "2025-04-08T10:51:02+00:00" }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_vpc_creates_default_sg": { "last_validated_date": "2024-04-01T11:21:54+00:00" From 072c810a86ebe66addd6c9ab6a8dbfc654fc2d31 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:37:38 -0600 Subject: [PATCH 062/108] revert removal of get_resource_type method (#12534) --- .../cloudformation/engine/template_deployer.py | 11 ++++++----- .../services/cloudformation/resource_provider.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py index 16d2fc88f95c1..a0ae9c286d61c 100644 --- a/localstack-core/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack-core/localstack/services/cloudformation/engine/template_deployer.py @@ -35,6 +35,7 @@ ProgressEvent, ResourceProviderExecutor, ResourceProviderPayload, + get_resource_type, ) from localstack.services.cloudformation.service_models import ( DependencyNotYetSatisfied, @@ -363,7 +364,7 @@ def _resolve_refs_recursively( ) resource = resources.get(resource_logical_id) - resource_type = resource["Type"] + resource_type = get_resource_type(resource) resolved_getatt = get_attr_from_model_instance( resource, attribute_name, @@ -811,7 +812,7 @@ def _replace(match): resolved = get_attr_from_model_instance( resources[logical_resource_id], attr_name, - resources[logical_resource_id]["Type"], + get_resource_type(resources[logical_resource_id]), logical_resource_id, ) if resolved is None: @@ -1294,7 +1295,7 @@ def apply_change(self, change: ChangeConfig, stack: Stack) -> None: action, logical_resource_id=resource_id ) - resource_provider = executor.try_load_resource_provider(resource["Type"]) + resource_provider = executor.try_load_resource_provider(get_resource_type(resource)) if resource_provider is not None: # add in-progress event resource_status = f"{get_action_name_for_resource_change(action)}_IN_PROGRESS" @@ -1406,7 +1407,7 @@ def delete_stack(self): resource["Properties"] = resource.get( "Properties", clone_safe(resource) ) # TODO: why is there a fallback? - resource["ResourceType"] = resource["Type"] + resource["ResourceType"] = get_resource_type(resource) ordered_resource_ids = list( order_resources( @@ -1437,7 +1438,7 @@ def delete_stack(self): len(resources), resource["ResourceType"], ) - resource_provider = executor.try_load_resource_provider(resource["Type"]) + resource_provider = executor.try_load_resource_provider(get_resource_type(resource)) if resource_provider is not None: event = executor.deploy_loop( resource_provider, resource, resource_provider_payload diff --git a/localstack-core/localstack/services/cloudformation/resource_provider.py b/localstack-core/localstack/services/cloudformation/resource_provider.py index ad590f2257385..7e48ed8ca5703 100644 --- a/localstack-core/localstack/services/cloudformation/resource_provider.py +++ b/localstack-core/localstack/services/cloudformation/resource_provider.py @@ -444,7 +444,7 @@ def deploy_loop( max_iterations = max(ceil(max_timeout / sleep_time), 2) for current_iteration in range(max_iterations): - resource_type = raw_payload["resourceType"] + resource_type = get_resource_type({"Type": raw_payload["resourceType"]}) resource["SpecifiedProperties"] = raw_payload["requestData"]["resourceProperties"] try: From 3ea87fea3497d87cd4a56fbd35aeabc1d2d284a7 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Thu, 17 Apr 2025 13:52:17 +0200 Subject: [PATCH 063/108] Add error handling if lambda logs are not received from the environment (#12521) --- .../lambda_/invocation/version_manager.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/invocation/version_manager.py b/localstack-core/localstack/services/lambda_/invocation/version_manager.py index f39d706c3f118..f8452816e1a7c 100644 --- a/localstack-core/localstack/services/lambda_/invocation/version_manager.py +++ b/localstack-core/localstack/services/lambda_/invocation/version_manager.py @@ -238,13 +238,20 @@ def invoke(self, *, invocation: Invocation) -> InvocationResult: ) # TODO: consider using the same prefix logging as in error case for execution environment. # possibly as separate named logger. - LOG.debug("Got logs for invocation '%s'", invocation.request_id) - for log_line in invocation_result.logs.splitlines(): - LOG.debug( - "[%s-%s] %s", - function_id.function_name, + if invocation_result.logs is not None: + LOG.debug("Got logs for invocation '%s'", invocation.request_id) + for log_line in invocation_result.logs.splitlines(): + LOG.debug( + "[%s-%s] %s", + function_id.function_name, + invocation.request_id, + truncate(log_line, config.LAMBDA_TRUNCATE_STDOUT), + ) + else: + LOG.warning( + "[%s] Error while printing logs for function '%s': Received no logs from environment.", invocation.request_id, - truncate(log_line, config.LAMBDA_TRUNCATE_STDOUT), + function_id.function_name, ) return invocation_result @@ -260,7 +267,8 @@ def store_logs( self.log_handler.add_logs(log_item) else: LOG.warning( - "Received no logs from invocation with id %s for lambda %s", + "Received no logs from invocation with id %s for lambda %s. Execution environment logs: \n%s", invocation_result.request_id, self.function_arn, + execution_env.get_prefixed_logs(), ) From 052eedfc3b47999562596121caeef64beb049799 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:11:09 +0200 Subject: [PATCH 064/108] Step Functions: Surface Support for Mocked Responses (#12525) --- .../state_task/mock_eval_utils.py | 45 ++ .../state_task/service/state_task_service.py | 20 +- .../stepfunctions/asl/eval/environment.py | 28 ++ .../stepfunctions/backend/execution.py | 6 + .../stepfunctions/backend/execution_worker.py | 5 + .../stepfunctions/mocking/mock_config.py | 271 ++++++++--- .../stepfunctions/mocking/mock_config_file.py | 150 ++++++ .../services/stepfunctions/provider.py | 25 +- .../testing/pytest/stepfunctions/utils.py | 62 ++- .../lambda/200_string_body.json5 | 1 - .../v2/mocking/test_base_scenarios.py | 83 ++++ .../mocking/test_base_scenarios.snapshot.json | 439 ++++++++++++++++++ .../test_base_scenarios.validation.json | 5 + .../v2/mocking/test_mock_config_file.py | 21 +- 14 files changed, 1083 insertions(+), 78 deletions(-) create mode 100644 localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/mock_eval_utils.py create mode 100644 localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/mock_eval_utils.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/mock_eval_utils.py new file mode 100644 index 0000000000000..aa8a9c423f433 --- /dev/null +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/mock_eval_utils.py @@ -0,0 +1,45 @@ +import copy + +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails +from localstack.services.stepfunctions.asl.component.common.error_name.custom_error_name import ( + CustomErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.mocking.mock_config import ( + MockedResponse, + MockedResponseReturn, + MockedResponseThrow, +) + + +def _eval_mocked_response_throw(env: Environment, mocked_response: MockedResponseThrow) -> None: + task_failed_event_details = TaskFailedEventDetails( + error=mocked_response.error, cause=mocked_response.cause + ) + error_name = CustomErrorName(mocked_response.error) + failure_event = FailureEvent( + env=env, + error_name=error_name, + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails(taskFailedEventDetails=task_failed_event_details), + ) + raise FailureEventException(failure_event=failure_event) + + +def _eval_mocked_response_return(env: Environment, mocked_response: MockedResponseReturn) -> None: + payload_copy = copy.deepcopy(mocked_response.payload) + env.stack.append(payload_copy) + + +def eval_mocked_response(env: Environment, mocked_response: MockedResponse) -> None: + if isinstance(mocked_response, MockedResponseReturn): + _eval_mocked_response_return(env=env, mocked_response=mocked_response) + elif isinstance(mocked_response, MockedResponseThrow): + _eval_mocked_response_throw(env=env, mocked_response=mocked_response) + else: + raise RuntimeError(f"Invalid MockedResponse type '{type(mocked_response)}'") diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py index b30c9c0e1e927..c385368c25dc2 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py @@ -33,6 +33,9 @@ from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.credentials import ( StateCredentials, ) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.mock_eval_utils import ( + eval_mocked_response, +) from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.resource import ( ResourceRuntimePart, ServiceResource, @@ -44,6 +47,7 @@ from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.services.stepfunctions.mocking.mock_config import MockedResponse from localstack.services.stepfunctions.quotas import is_within_size_quota from localstack.utils.strings import camel_to_snake_case, snake_to_camel_case, to_bytes, to_str @@ -352,12 +356,16 @@ def _eval_execution(self, env: Environment) -> None: normalised_parameters = copy.deepcopy(raw_parameters) self._normalise_parameters(normalised_parameters) - self._eval_service_task( - env=env, - resource_runtime_part=resource_runtime_part, - normalised_parameters=normalised_parameters, - state_credentials=state_credentials, - ) + if env.is_mocked_mode(): + mocked_response: MockedResponse = env.get_current_mocked_response() + eval_mocked_response(env=env, mocked_response=mocked_response) + else: + self._eval_service_task( + env=env, + resource_runtime_part=resource_runtime_part, + normalised_parameters=normalised_parameters, + state_credentials=state_credentials, + ) output_value = env.stack[-1] self._normalise_response(output_value) diff --git a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py index c397ce86ba300..735276c68c2ff 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py +++ b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py @@ -34,6 +34,7 @@ from localstack.services.stepfunctions.asl.eval.states import ContextObjectData, States from localstack.services.stepfunctions.asl.eval.variable_store import VariableStore from localstack.services.stepfunctions.backend.activity import Activity +from localstack.services.stepfunctions.mocking.mock_config import MockedResponse, MockTestCase LOG = logging.getLogger(__name__) @@ -51,6 +52,7 @@ class Environment: callback_pool_manager: CallbackPoolManager map_run_record_pool_manager: MapRunRecordPoolManager activity_store: Final[dict[Arn, Activity]] + mock_test_case: Optional[MockTestCase] = None _frames: Final[list[Environment]] _is_frame: bool = False @@ -69,6 +71,7 @@ def __init__( cloud_watch_logging_session: Optional[CloudWatchLoggingSession], activity_store: dict[Arn, Activity], variable_store: Optional[VariableStore] = None, + mock_test_case: Optional[MockTestCase] = None, ): super(Environment, self).__init__() self._state_mutex = threading.RLock() @@ -86,6 +89,8 @@ def __init__( self.activity_store = activity_store + self.mock_test_case = mock_test_case + self._frames = list() self._is_frame = False @@ -133,6 +138,7 @@ def as_inner_frame_of( cloud_watch_logging_session=env.cloud_watch_logging_session, activity_store=env.activity_store, variable_store=variable_store, + mock_test_case=env.mock_test_case, ) frame._is_frame = True frame.event_manager = env.event_manager @@ -262,3 +268,25 @@ def is_frame(self) -> bool: def is_standard_workflow(self) -> bool: return self.execution_type == StateMachineType.STANDARD + + def is_mocked_mode(self) -> bool: + return self.mock_test_case is not None + + def get_current_mocked_response(self) -> MockedResponse: + if not self.is_mocked_mode(): + raise RuntimeError( + "Cannot retrieve mocked response: execution is not operating in mocked mode" + ) + state_name = self.next_state_name + state_mocked_responses: Optional = self.mock_test_case.state_mocked_responses.get( + state_name + ) + if state_mocked_responses is None: + raise RuntimeError(f"No mocked response definition for state '{state_name}'") + retry_count = self.states.context_object.context_object_data["State"]["RetryCount"] + if len(state_mocked_responses.mocked_responses) <= retry_count: + raise RuntimeError( + f"No mocked response definition for state '{state_name}' " + f"and retry number '{retry_count}'" + ) + return state_mocked_responses.mocked_responses[retry_count] diff --git a/localstack-core/localstack/services/stepfunctions/backend/execution.py b/localstack-core/localstack/services/stepfunctions/backend/execution.py index 5f3c5aeba87d3..76090c7981944 100644 --- a/localstack-core/localstack/services/stepfunctions/backend/execution.py +++ b/localstack-core/localstack/services/stepfunctions/backend/execution.py @@ -59,6 +59,7 @@ StateMachineInstance, StateMachineVersion, ) +from localstack.services.stepfunctions.mocking.mock_config import MockTestCase LOG = logging.getLogger(__name__) @@ -107,6 +108,8 @@ class Execution: state_machine_version_arn: Final[Optional[Arn]] state_machine_alias_arn: Final[Optional[Arn]] + mock_test_case: Final[Optional[MockTestCase]] + start_date: Final[Timestamp] input_data: Final[Optional[json]] input_details: Final[Optional[CloudWatchEventsExecutionDataDetails]] @@ -141,6 +144,7 @@ def __init__( input_data: Optional[json] = None, trace_header: Optional[TraceHeader] = None, state_machine_alias_arn: Optional[Arn] = None, + mock_test_case: Optional[MockTestCase] = None, ): self.name = name self.sm_type = sm_type @@ -169,6 +173,7 @@ def __init__( self.error = None self.cause = None self._activity_store = activity_store + self.mock_test_case = mock_test_case def _get_events_client(self): return connect_to(aws_access_key_id=self.account_id, region_name=self.region_name).events @@ -301,6 +306,7 @@ def _get_start_execution_worker(self) -> ExecutionWorker: exec_comm=self._get_start_execution_worker_comm(), cloud_watch_logging_session=self._cloud_watch_logging_session, activity_store=self._activity_store, + mock_test_case=self.mock_test_case, ) def start(self) -> None: diff --git a/localstack-core/localstack/services/stepfunctions/backend/execution_worker.py b/localstack-core/localstack/services/stepfunctions/backend/execution_worker.py index 86284dce13a84..c2d14c2085295 100644 --- a/localstack-core/localstack/services/stepfunctions/backend/execution_worker.py +++ b/localstack-core/localstack/services/stepfunctions/backend/execution_worker.py @@ -29,6 +29,7 @@ from localstack.services.stepfunctions.backend.execution_worker_comm import ( ExecutionWorkerCommunication, ) +from localstack.services.stepfunctions.mocking.mock_config import MockTestCase from localstack.utils.common import TMP_THREADS @@ -36,6 +37,7 @@ class ExecutionWorker: _evaluation_details: Final[EvaluationDetails] _execution_communication: Final[ExecutionWorkerCommunication] _cloud_watch_logging_session: Final[Optional[CloudWatchLoggingSession]] + _mock_test_case: Final[Optional[MockTestCase]] _activity_store: dict[Arn, Activity] env: Optional[Environment] @@ -46,10 +48,12 @@ def __init__( exec_comm: ExecutionWorkerCommunication, cloud_watch_logging_session: Optional[CloudWatchLoggingSession], activity_store: dict[Arn, Activity], + mock_test_case: Optional[MockTestCase] = None, ): self._evaluation_details = evaluation_details self._execution_communication = exec_comm self._cloud_watch_logging_session = cloud_watch_logging_session + self._mock_test_case = mock_test_case self._activity_store = activity_store self.env = None @@ -78,6 +82,7 @@ def _get_evaluation_environment(self) -> Environment: event_history_context=EventHistoryContext.of_program_start(), cloud_watch_logging_session=self._cloud_watch_logging_session, activity_store=self._activity_store, + mock_test_case=self._mock_test_case, ) def _execution_logic(self): diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py index f76b80c358b96..f69b27eba6c55 100644 --- a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py @@ -1,69 +1,214 @@ -import json -import logging -import os -from functools import lru_cache -from typing import Optional - -from localstack import config - -LOG = logging.getLogger(__name__) - - -@lru_cache(maxsize=1) -def _retrieve_sfn_mock_config(file_path: str, modified_epoch: int) -> Optional[dict]: # noqa - """ - Load and cache the Step Functions mock configuration from a JSON file. - - This function is memoized using `functools.lru_cache` to avoid re-reading the file - from disk unless it has changed. The `modified_epoch` parameter is used solely to - trigger cache invalidation when the file is updated. If either the file path or the - modified timestamp changes, the cached result is discarded and the file is reloaded. - - Parameters: - file_path (str): - The absolute path to the JSON configuration file. - - modified_epoch (int): - The last modified time of the file, in epoch seconds. This value is used - as part of the cache key to ensure the cache is refreshed when the file is updated. - - Returns: - Optional[dict]: - The parsed configuration as a dictionary if the file is successfully loaded, - or `None` if an error occurs during reading or parsing. - - Notes: - - The `modified_epoch` argument is not used inside the function logic, but is - necessary to ensure cache correctness via `lru_cache`. - - Logging is used to capture warnings if file access or parsing fails. - """ - try: - with open(file_path, "r") as df: - mock_config = json.load(df) - return mock_config - except Exception as ex: - LOG.warning( - "Unable to load step functions mock configuration file at '%s' due to %s", - file_path, - ex, +import abc +from typing import Any, Final, Optional + +from localstack.services.stepfunctions.mocking.mock_config_file import ( + RawMockConfig, + RawResponseModel, + RawTestCase, + _load_sfn_raw_mock_config, +) + + +class MockedResponse(abc.ABC): + range_start: Final[int] + range_end: Final[int] + + def __init__(self, range_start: int, range_end: int): + super().__init__() + if range_start < 0 or range_end < 0: + raise ValueError( + f"Invalid range: both '{range_start}' and '{range_end}' must be positive integers." + ) + if range_start != range_end and range_end < range_start + 1: + raise ValueError( + f"Invalid range: values must be equal or '{range_start}' " + f"must be at least one greater than '{range_end}'." + ) + self.range_start = range_start + self.range_end = range_end + + +class MockedResponseReturn(MockedResponse): + payload: Final[dict[Any, Any]] + + def __init__(self, range_start: int, range_end: int, payload: dict[Any, Any]): + super().__init__(range_start=range_start, range_end=range_end) + self.payload = payload + + +class MockedResponseThrow(MockedResponse): + error: Final[str] + cause: Final[str] + + def __init__(self, range_start: int, range_end: int, error: str, cause: str): + super().__init__(range_start=range_start, range_end=range_end) + self.error = error + self.cause = cause + + +class StateMockedResponses: + state_name: Final[str] + mocked_response_name: Final[str] + mocked_responses: Final[list[MockedResponse]] + + def __init__( + self, state_name: str, mocked_response_name: str, mocked_responses: list[MockedResponse] + ): + self.state_name = state_name + self.mocked_response_name = mocked_response_name + self.mocked_responses = list() + last_range_end: int = 0 + mocked_responses_sorted = sorted(mocked_responses, key=lambda mr: mr.range_start) + for mocked_response in mocked_responses_sorted: + if not mocked_response.range_start - last_range_end == 0: + raise RuntimeError( + f"Inconsistent event numbering detected for state '{state_name}': " + f"the previous mocked response ended at event '{last_range_end}' " + f"while the next response '{mocked_response_name}' " + f"starts at event '{mocked_response.range_start}'. " + "Mock responses must be consecutively numbered. " + f"Expected the next response to begin at event {last_range_end + 1}." + ) + repeats = mocked_response.range_start - mocked_response.range_end + 1 + self.mocked_responses.extend([mocked_response] * repeats) + last_range_end = mocked_response.range_end + + +class MockTestCase: + state_machine_name: Final[str] + test_case_name: Final[str] + state_mocked_responses: Final[dict[str, StateMockedResponses]] + + def __init__( + self, + state_machine_name: str, + test_case_name: str, + state_mocked_responses_list: list[StateMockedResponses], + ): + self.state_machine_name = state_machine_name + self.test_case_name = test_case_name + self.state_mocked_responses = dict() + for state_mocked_response in state_mocked_responses_list: + state_name = state_mocked_response.state_name + if state_name in self.state_mocked_responses: + raise RuntimeError( + f"Duplicate definition of state '{state_name}' for test case '{test_case_name}'" + ) + self.state_mocked_responses[state_name] = state_mocked_response + + +def _parse_mocked_response_range(string_definition: str) -> tuple[int, int]: + definition_parts = string_definition.strip().split("-") + if len(definition_parts) == 1: + range_part = definition_parts[0] + try: + range_value = int(range_part) + return range_value, range_value + except Exception: + raise RuntimeError( + f"Unknown mocked response retry range value '{range_part}', not a valid integer" + ) + elif len(definition_parts) == 2: + range_part_start = definition_parts[0] + range_part_end = definition_parts[1] + try: + return int(range_part_start), int(range_part_end) + except Exception: + raise RuntimeError( + f"Unknown mocked response retry range value '{range_part_start}:{range_part_end}', " + "not valid integer values" + ) + else: + raise RuntimeError( + f"Unknown mocked response retry range definition '{string_definition}', " + "range definition should consist of one integer (e.g. '0'), or a integer range (e.g. '1-2')'." ) - return None -def load_sfn_mock_config_file() -> Optional[dict]: - configuration_file_path = config.SFN_MOCK_CONFIG - if not configuration_file_path: - return None +def _mocked_response_from_raw( + raw_response_model_range: str, raw_response_model: RawResponseModel +) -> MockedResponse: + range_start, range_end = _parse_mocked_response_range(raw_response_model_range) + if raw_response_model.Return: + payload = raw_response_model.Return.model_dump() + return MockedResponseReturn(range_start=range_start, range_end=range_end, payload=payload) + throw_definition = raw_response_model.Throw + return MockedResponseThrow( + range_start=range_start, + range_end=range_end, + error=throw_definition.Error, + cause=throw_definition.Cause, + ) - try: - modified_time = int(os.path.getmtime(configuration_file_path)) - except Exception as ex: - LOG.warning( - "Unable to access the step functions mock configuration file at '%s' due to %s", - configuration_file_path, - ex, + +def _mocked_responses_from_raw( + mocked_response_name: str, raw_mock_config: RawMockConfig +) -> list[MockedResponse]: + raw_response_models: Optional[dict[str, RawResponseModel]] = ( + raw_mock_config.MockedResponses.get(mocked_response_name) + ) + if not raw_response_models: + raise RuntimeError( + f"No definitions for mocked response '{mocked_response_name}' in the mock configuration file." ) - return None + mocked_responses: list[MockedResponse] = list() + for raw_response_model_range, raw_response_model in raw_response_models.items(): + mocked_response: MockedResponse = _mocked_response_from_raw( + raw_response_model_range=raw_response_model_range, raw_response_model=raw_response_model + ) + mocked_responses.append(mocked_response) + return mocked_responses + - mock_config = _retrieve_sfn_mock_config(configuration_file_path, modified_time) - return mock_config +def _state_mocked_responses_from_raw( + state_name: str, mocked_response_name: str, raw_mock_config: RawMockConfig +) -> StateMockedResponses: + mocked_responses = _mocked_responses_from_raw( + mocked_response_name=mocked_response_name, raw_mock_config=raw_mock_config + ) + return StateMockedResponses( + state_name=state_name, + mocked_response_name=mocked_response_name, + mocked_responses=mocked_responses, + ) + + +def _mock_test_case_from_raw( + state_machine_name: str, test_case_name: str, raw_mock_config: RawMockConfig +) -> MockTestCase: + state_machine = raw_mock_config.StateMachines.get(state_machine_name) + if not state_machine: + raise RuntimeError( + f"No definitions for state machine '{state_machine_name}' in the mock configuration file." + ) + test_case: RawTestCase = state_machine.TestCases.get(test_case_name) + if not test_case: + raise RuntimeError( + f"No definitions for test case '{test_case_name}' and " + f"state machine '{state_machine_name}' in the mock configuration file." + ) + state_mocked_responses_list: list[StateMockedResponses] = list() + for state_name, mocked_response_name in test_case.root.items(): + state_mocked_responses = _state_mocked_responses_from_raw( + state_name=state_name, + mocked_response_name=mocked_response_name, + raw_mock_config=raw_mock_config, + ) + state_mocked_responses_list.append(state_mocked_responses) + return MockTestCase( + state_machine_name=state_machine_name, + test_case_name=test_case_name, + state_mocked_responses_list=state_mocked_responses_list, + ) + + +def load_mock_test_case_for(state_machine_name: str, test_case_name: str) -> Optional[MockTestCase]: + raw_mock_config: Optional[RawMockConfig] = _load_sfn_raw_mock_config() + if raw_mock_config is None: + return None + mock_test_case: MockTestCase = _mock_test_case_from_raw( + state_machine_name=state_machine_name, + test_case_name=test_case_name, + raw_mock_config=raw_mock_config, + ) + return mock_test_case diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py new file mode 100644 index 0000000000000..c33578f330649 --- /dev/null +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py @@ -0,0 +1,150 @@ +import logging +import os +from functools import lru_cache +from typing import Dict, Final, Optional + +from pydantic import BaseModel, RootModel, model_validator + +from localstack import config + +LOG = logging.getLogger(__name__) + +_RETURN_KEY: Final[str] = "Return" +_THROW_KEY: Final[str] = "Throw" + + +class RawReturnResponse(BaseModel): + """ + Represents a return response. + Accepts any fields. + """ + + model_config = {"extra": "allow", "frozen": True} + + +class RawThrowResponse(BaseModel): + """ + Represents an error response. + Both 'Error' and 'Cause' are required. + """ + + model_config = {"frozen": True} + + Error: str + Cause: str + + +class RawResponseModel(BaseModel): + """ + A response step must include exactly one of: + - 'Return': a ReturnResponse object. + - 'Throw': a ThrowResponse object. + """ + + model_config = {"frozen": True} + + Return: Optional[RawReturnResponse] = None + Throw: Optional[RawThrowResponse] = None + + @model_validator(mode="before") + def validate_response(cls, data: dict) -> dict: + if _RETURN_KEY in data and _THROW_KEY in data: + raise ValueError(f"Response cannot contain both '{_RETURN_KEY}' and '{_THROW_KEY}'") + if _RETURN_KEY not in data and _THROW_KEY not in data: + raise ValueError(f"Response must contain one of '{_RETURN_KEY}' or '{_THROW_KEY}'") + return data + + +class RawTestCase(RootModel[Dict[str, str]]): + """ + Represents an individual test case. + The keys are state names (e.g., 'LambdaState', 'SQSState') + and the values are the names of the mocked response configurations. + """ + + model_config = {"frozen": True} + + +class RawStateMachine(BaseModel): + """ + Represents a state machine configuration containing multiple test cases. + """ + + model_config = {"frozen": True} + + TestCases: Dict[str, RawTestCase] + + +class RawMockConfig(BaseModel): + """ + The root configuration that contains: + - StateMachines: mapping state machine names to their configuration. + - MockedResponses: mapping response configuration names to response steps. + Each response step is keyed (e.g. "0", "1-2") and maps to a ResponseModel. + """ + + model_config = {"frozen": True} + + StateMachines: Dict[str, RawStateMachine] + MockedResponses: Dict[str, Dict[str, RawResponseModel]] + + +@lru_cache(maxsize=1) +def _read_sfn_raw_mock_config(file_path: str, modified_epoch: int) -> Optional[RawMockConfig]: # noqa + """ + Load and cache the Step Functions mock configuration from a JSON file. + + This function is memoized using `functools.lru_cache` to avoid re-reading the file + from disk unless it has changed. The `modified_epoch` parameter is used solely to + trigger cache invalidation when the file is updated. If either the file path or the + modified timestamp changes, the cached result is discarded and the file is reloaded. + + Parameters: + file_path (str): + The absolute path to the JSON configuration file. + + modified_epoch (int): + The last modified time of the file, in epoch seconds. This value is used + as part of the cache key to ensure the cache is refreshed when the file is updated. + + Returns: + Optional[dict]: + The parsed configuration as a dictionary if the file is successfully loaded, + or `None` if an error occurs during reading or parsing. + + Notes: + - The `modified_epoch` argument is not used inside the function logic, but is + necessary to ensure cache correctness via `lru_cache`. + - Logging is used to capture warnings if file access or parsing fails. + """ + try: + with open(file_path, "r") as df: + mock_config_str = df.read() + mock_config: RawMockConfig = RawMockConfig.model_validate_json(mock_config_str) + return mock_config + except Exception as ex: + LOG.warning( + "Unable to load step functions mock configuration file at '%s' due to %s", + file_path, + ex, + ) + return None + + +def _load_sfn_raw_mock_config() -> Optional[RawMockConfig]: + configuration_file_path = config.SFN_MOCK_CONFIG + if not configuration_file_path: + return None + + try: + modified_time = int(os.path.getmtime(configuration_file_path)) + except Exception as ex: + LOG.warning( + "Unable to access the step functions mock configuration file at '%s' due to %s", + configuration_file_path, + ex, + ) + return None + + mock_config = _read_sfn_raw_mock_config(configuration_file_path, modified_time) + return mock_config diff --git a/localstack-core/localstack/services/stepfunctions/provider.py b/localstack-core/localstack/services/stepfunctions/provider.py index bf59a7c69949d..95c04b4887e71 100644 --- a/localstack-core/localstack/services/stepfunctions/provider.py +++ b/localstack-core/localstack/services/stepfunctions/provider.py @@ -150,6 +150,10 @@ from localstack.services.stepfunctions.backend.test_state.execution import ( TestStateExecution, ) +from localstack.services.stepfunctions.mocking.mock_config import ( + MockTestCase, + load_mock_test_case_for, +) from localstack.services.stepfunctions.stepfunctions_utils import ( assert_pagination_parameters_valid, get_next_page_token_from_arn, @@ -180,7 +184,7 @@ def accept_state_visitor(self, visitor: StateVisitor): visitor.visit(sfn_stores) _STATE_MACHINE_ARN_REGEX: Final[re.Pattern] = re.compile( - rf"{ARN_PARTITION_REGEX}:states:[a-z0-9-]+:[0-9]{{12}}:stateMachine:[a-zA-Z0-9-_.]+(:\d+)?(:[a-zA-Z0-9-_.]+)*$" + rf"{ARN_PARTITION_REGEX}:states:[a-z0-9-]+:[0-9]{{12}}:stateMachine:[a-zA-Z0-9-_.]+(:\d+)?(:[a-zA-Z0-9-_.]+)*(?:#[a-zA-Z0-9-_]+)?$" ) _STATE_MACHINE_EXECUTION_ARN_REGEX: Final[re.Pattern] = re.compile( @@ -779,6 +783,12 @@ def start_execution( ) -> StartExecutionOutput: self._validate_state_machine_arn(state_machine_arn) + state_machine_arn_parts = state_machine_arn.split("#") + state_machine_arn = state_machine_arn_parts[0] + mock_test_case_name = ( + state_machine_arn_parts[1] if len(state_machine_arn_parts) == 2 else None + ) + store = self.get_store(context=context) alias: Optional[Alias] = store.aliases.get(state_machine_arn) @@ -832,6 +842,18 @@ def start_execution( configuration=state_machine_clone.cloud_watch_logging_configuration, ) + mock_test_case: Optional[MockTestCase] = None + if mock_test_case_name is not None: + state_machine_name = state_machine_clone.name + mock_test_case = load_mock_test_case_for( + state_machine_name=state_machine_name, test_case_name=mock_test_case_name + ) + if mock_test_case is None: + raise InvalidName( + f"Invalid mock test case name '{mock_test_case_name}' " + f"for state machine '{state_machine_name}'" + ) + execution = Execution( name=exec_name, sm_type=state_machine_clone.sm_type, @@ -846,6 +868,7 @@ def start_execution( input_data=input_data, trace_header=trace_header, activity_store=self.get_store(context).activities, + mock_test_case=mock_test_case, ) store.executions[exec_arn] = execution diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py index ddc0d6f6e41fe..bb55f99bc3958 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py @@ -383,6 +383,7 @@ def create_state_machine_with_iam_role( snapshot, definition: Definition, logging_configuration: Optional[LoggingConfiguration] = None, + state_machine_name: Optional[str] = None, ): snf_role_arn = create_state_machine_iam_role(target_aws_client=target_aws_client) snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -396,7 +397,7 @@ def create_state_machine_with_iam_role( RegexTransformer("Request ID: [a-zA-Z0-9-]+", "Request ID: ") ) - sm_name: str = f"statemachine_create_and_record_execution_{short_uid()}" + sm_name: str = state_machine_name or f"statemachine_create_and_record_execution_{short_uid()}" create_arguments = { "name": sm_name, "definition": definition, @@ -450,6 +451,42 @@ def launch_and_record_execution( return execution_arn +def launch_and_record_mocked_execution( + target_aws_client, + sfn_snapshot, + state_machine_arn, + execution_input, + test_name, +) -> LongArn: + stepfunctions_client = target_aws_client.stepfunctions + exec_resp = stepfunctions_client.start_execution( + stateMachineArn=f"{state_machine_arn}#{test_name}", input=execution_input + ) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_exec_arn(exec_resp, 0)) + execution_arn = exec_resp["executionArn"] + + await_execution_terminated( + stepfunctions_client=stepfunctions_client, execution_arn=execution_arn + ) + + get_execution_history = stepfunctions_client.get_execution_history(executionArn=execution_arn) + + # Transform all map runs if any. + try: + map_run_arns = extract_json("$..mapRunArn", get_execution_history) + if isinstance(map_run_arns, str): + map_run_arns = [map_run_arns] + for i, map_run_arn in enumerate(list(set(map_run_arns))): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_map_run_arn(map_run_arn, i)) + except NoSuchJsonPathError: + # No mapRunArns + pass + + sfn_snapshot.match("get_execution_history", get_execution_history) + + return execution_arn + + def launch_and_record_logs( target_aws_client, state_machine_arn, @@ -513,6 +550,29 @@ def create_and_record_execution( ) +def create_and_record_mocked_execution( + target_aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + execution_input, + state_machine_name, + test_name, +): + state_machine_arn = create_state_machine_with_iam_role( + target_aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + state_machine_name=state_machine_name, + ) + launch_and_record_mocked_execution( + target_aws_client, sfn_snapshot, state_machine_arn, execution_input, test_name + ) + + def create_and_record_logs( target_aws_client, create_state_machine_iam_role, diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 index 895c7c06d59eb..8fdb0ae4aecb4 100644 --- a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 @@ -3,7 +3,6 @@ "Return": { "StatusCode": 200, "Payload": { - "StatusCode": 200, "body": "string body" } } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py new file mode 100644 index 0000000000000..555047aa59843 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py @@ -0,0 +1,83 @@ +import json + +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack import config +from localstack.aws.api.lambda_ import Runtime +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, + create_and_record_mocked_execution, +) +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( + MockedResponseLoader, +) +from tests.aws.services.stepfunctions.templates.services.services_templates import ( + ServicesTemplates as ST, +) + + +@markers.snapshot.skip_snapshot_verify( + paths=["$..SdkHttpMetadata", "$..SdkResponseMetadata", "$..ExecutedVersion"] +) +class TestBaseScenarios: + @markers.aws.validated + def test_lambda_service_invoke( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + create_lambda_function, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + template = ST.load_sfn_template(ST.LAMBDA_INVOKE) + definition = json.dumps(template) + + function_name = f"lambda_{short_uid()}" + sfn_snapshot.add_transformer(RegexTransformer(function_name, "lambda_function_name")) + exec_input = json.dumps({"FunctionName": function_name, "Payload": {"body": "string body"}}) + + if is_aws_cloud(): + create_lambda_function( + func_name=function_name, + handler_file=ST.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + lambda_200_string_body = MockedResponseLoader.load( + MockedResponseLoader.LAMBDA_200_STRING_BODY + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"Start": "lambda_200_string_body"}} + } + }, + "MockedResponses": {"lambda_200_string_body": lambda_200_string_body}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json new file mode 100644 index 0000000000000..a930fbede3dc9 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json @@ -0,0 +1,439 @@ +{ + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_lambda_service_invoke": { + "recorded-date": "14-04-2025, 18:51:50", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "FunctionName": "lambda_function_name", + "Payload": { + "body": "string body" + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "FunctionName": "lambda_function_name", + "Payload": { + "body": "string body" + } + }, + "inputDetails": { + "truncated": false + }, + "name": "Start" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "FunctionName": "lambda_function_name", + "Payload": { + "body": "string body" + } + }, + "region": "", + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + }, + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "Start", + "output": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": 7, + "previousEventId": 6, + "stateEnteredEventDetails": { + "input": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "inputDetails": { + "truncated": false + }, + "name": "EndWithFinal" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 8, + "previousEventId": 7, + "stateExitedEventDetails": { + "name": "EndWithFinal", + "output": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200, + "final": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200, + "final": { + "ExecutedVersion": "$LATEST", + "Payload": { + "body": "string body" + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "23" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "23", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 9, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json new file mode 100644 index 0000000000000..cc7c6bc4de8e5 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json @@ -0,0 +1,5 @@ +{ + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_lambda_service_invoke": { + "last_validated_date": "2025-04-14T18:51:50+00:00" + } +} diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py index 7fa346acec8ea..f460e96b107e6 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py @@ -1,5 +1,8 @@ from localstack import config -from localstack.services.stepfunctions.mocking.mock_config import load_sfn_mock_config_file +from localstack.services.stepfunctions.mocking.mock_config import ( + MockTestCase, + load_mock_test_case_for, +) from localstack.testing.pytest import markers from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( MockedResponseLoader, @@ -9,8 +12,10 @@ class TestMockConfigFile: @markers.aws.only_localstack def test_is_mock_config_flag_detected_unset(self, mock_config_file): - loaded_mock_config_file = load_sfn_mock_config_file() - assert loaded_mock_config_file is None + mock_test_case = load_mock_test_case_for( + state_machine_name="state_machine_name", test_case_name="test_case_name" + ) + assert mock_test_case is None @markers.aws.only_localstack def test_is_mock_config_flag_detected_set(self, mock_config_file, monkeypatch): @@ -19,10 +24,14 @@ def test_is_mock_config_flag_detected_set(self, mock_config_file, monkeypatch): ) # TODO: add typing for MockConfigFile.json components mock_config = { - "StateMachines": {"S0": {"TestCases": {"LambdaState": "lambda_200_string_body"}}}, + "StateMachines": { + "S0": {"TestCases": {"BaseTestCase": {"LambdaState": "lambda_200_string_body"}}} + }, "MockedResponses": {"lambda_200_string_body": lambda_200_string_body}, } mock_config_file_path = mock_config_file(mock_config) monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) - loaded_mock_config_file = load_sfn_mock_config_file() - assert loaded_mock_config_file == mock_config + mock_test_case: MockTestCase = load_mock_test_case_for( + state_machine_name="S0", test_case_name="BaseTestCase" + ) + assert mock_test_case is not None From 5e8dc099343aeb4eb65f07881bb11e80c7075137 Mon Sep 17 00:00:00 2001 From: Tjeerd Ritsma Date: Fri, 18 Apr 2025 10:38:43 +0200 Subject: [PATCH 065/108] apply fix for podman container labels dict (#12526) Co-authored-by: Tjeerd Ritsma --- .../localstack/utils/container_utils/docker_cmd_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/utils/container_utils/docker_cmd_client.py b/localstack-core/localstack/utils/container_utils/docker_cmd_client.py index 4ca0f2d26a8c4..7cdd7b59f8092 100644 --- a/localstack-core/localstack/utils/container_utils/docker_cmd_client.py +++ b/localstack-core/localstack/utils/container_utils/docker_cmd_client.py @@ -909,12 +909,15 @@ def _check_and_raise_no_such_container_error( if any(msg.lower() in process_stdout_lower for msg in error_messages): raise NoSuchContainer(container_name_or_id, stdout=error.stdout, stderr=error.stderr) - def _transform_container_labels(self, labels: str) -> Dict[str, str]: + def _transform_container_labels(self, labels: Union[str, Dict[str, str]]) -> Dict[str, str]: """ Transforms the container labels returned by the docker command from the key-value pair format to a dict :param labels: Input string, comma separated key value pairs. Example: key1=value1,key2=value2 :return: Dict representation of the passed values, example: {"key1": "value1", "key2": "value2"} """ + if isinstance(labels, Dict): + return labels + labels = labels.split(",") labels = [label.partition("=") for label in labels] return {label[0]: label[2] for label in labels} From 4d7a0311f4420469bf86a64c50bf57e746479ec5 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 22 Apr 2025 12:23:25 +0530 Subject: [PATCH 066/108] fix links for issue & PR messages (#12541) --- .github/workflows/pr-welcome-first-time-contributors.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-welcome-first-time-contributors.yml b/.github/workflows/pr-welcome-first-time-contributors.yml index a68fedb4dc899..c01b376ececde 100644 --- a/.github/workflows/pr-welcome-first-time-contributors.yml +++ b/.github/workflows/pr-welcome-first-time-contributors.yml @@ -16,8 +16,8 @@ jobs: with: github-token: ${{ secrets.PRO_ACCESS_TOKEN }} script: | - const issueMessage = `Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).`; - const prMessage = `Welcome to LocalStack! Thanks for raising your first Pull Request and landing in your contributions. Our team will reach out with any reviews or feedbacks that we have shortly. We recommend joining our [Slack Community](https://localstack.cloud/contact/) and share your PR on the **#community** channel to share your contributions with us. Please make sure you are following our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [Code of Conduct](https://github.com/localstack/.github/blob/main/CODE_OF_CONDUCT.md).`; + const issueMessage = `Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/slack/) for real-time help and drop a message to [LocalStack Support](https://docs.localstack.cloud/getting-started/help-and-support/) if you are a licensed user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md).`; + const prMessage = `Welcome to LocalStack! Thanks for raising your first Pull Request and landing in your contributions. Our team will reach out with any reviews or feedbacks that we have shortly. We recommend joining our [Slack Community](https://localstack.cloud/slack/) and share your PR on the **#community** channel to share your contributions with us. Please make sure you are following our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [Code of Conduct](https://github.com/localstack/.github/blob/main/CODE_OF_CONDUCT.md).`; if (!issueMessage && !prMessage) { throw new Error('Action should have either issueMessage or prMessage set'); From 96a0216cd174bbbb49467ed4710bcf0e4990754b Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 22 Apr 2025 07:52:41 +0000 Subject: [PATCH 067/108] Admin: Enable mypy during linting (#12532) --- .pre-commit-config.yaml | 6 + Makefile | 1 + localstack-core/localstack/config.py | 2 +- localstack-core/localstack/constants.py | 2 +- localstack-core/localstack/packages/api.py | 56 +++++---- localstack-core/localstack/packages/core.py | 10 +- .../localstack/packages/debugpy.py | 8 +- localstack-core/localstack/packages/ffmpeg.py | 12 +- localstack-core/localstack/packages/java.py | 23 ++-- .../localstack/packages/plugins.py | 13 ++- .../localstack/packages/terraform.py | 10 +- .../localstack/services/kinesis/packages.py | 6 +- localstack-core/localstack/utils/archives.py | 25 ++-- localstack-core/localstack/utils/venv.py | 4 +- localstack-core/mypy.ini | 19 +++ mypy.ini | 10 -- pyproject.toml | 1 + requirements-dev.txt | 5 + requirements-typehint.txt | 109 +----------------- 19 files changed, 134 insertions(+), 188 deletions(-) create mode 100644 localstack-core/mypy.ini delete mode 100644 mypy.ini diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7213524a4b045..c33108fb7cdb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,12 @@ repos: # Run the formatter. - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.15.0 + hooks: + - id: mypy + entry: bash -c 'cd localstack-core && mypy --install-types --non-interactive' + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: diff --git a/Makefile b/Makefile index f594d468cc3b6..36f8d4d7598da 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,7 @@ lint: ## Run code linter to check code style, check if formatte ($(VENV_RUN); python -m ruff check --output-format=full . && python -m ruff format --check --diff .) $(VENV_RUN); pre-commit run check-pinned-deps-for-needed-upgrade --files pyproject.toml # run pre-commit hook manually here to ensure that this check runs in CI as well $(VENV_RUN); openapi-spec-validator localstack-core/localstack/openapi.yaml + $(VENV_RUN); cd localstack-core && mypy --install-types --non-interactive lint-modified: ## Run code linter to check code style, check if formatter would make changes on modified files, and check if dependency pins need to be updated because of modified files ($(VENV_RUN); python -m ruff check --output-format=full `git diff --diff-filter=d --name-only HEAD | grep '\.py$$' | xargs` && python -m ruff format --check `git diff --diff-filter=d --name-only HEAD | grep '\.py$$' | xargs`) diff --git a/localstack-core/localstack/config.py b/localstack-core/localstack/config.py index f4470356b7130..89583165b8787 100644 --- a/localstack-core/localstack/config.py +++ b/localstack-core/localstack/config.py @@ -605,7 +605,7 @@ def _get_unprivileged_port_range_start(self) -> int: def is_unprivileged(self) -> bool: return self.port >= self._get_unprivileged_port_range_start() - def host_and_port(self): + def host_and_port(self) -> str: formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host diff --git a/localstack-core/localstack/constants.py b/localstack-core/localstack/constants.py index 57406fa09e6a8..f5d43d2bab1e9 100644 --- a/localstack-core/localstack/constants.py +++ b/localstack-core/localstack/constants.py @@ -45,7 +45,7 @@ LOCALSTACK_ROOT_FOLDER = os.path.realpath(os.path.join(MODULE_MAIN_PATH, "..")) # virtualenv folder -LOCALSTACK_VENV_FOLDER = os.environ.get("VIRTUAL_ENV") +LOCALSTACK_VENV_FOLDER: str = os.environ.get("VIRTUAL_ENV") if not LOCALSTACK_VENV_FOLDER: # fallback to the previous logic LOCALSTACK_VENV_FOLDER = os.path.join(LOCALSTACK_ROOT_FOLDER, ".venv") diff --git a/localstack-core/localstack/packages/api.py b/localstack-core/localstack/packages/api.py index b3260e9c5b83f..2ad3280e2d71c 100644 --- a/localstack-core/localstack/packages/api.py +++ b/localstack-core/localstack/packages/api.py @@ -6,9 +6,9 @@ from enum import Enum from inspect import getmodule from threading import RLock -from typing import Callable, List, Optional, Tuple +from typing import Any, Callable, Generic, List, Optional, ParamSpec, TypeVar -from plux import Plugin, PluginManager, PluginSpec +from plux import Plugin, PluginManager, PluginSpec # type: ignore[import-untyped] from localstack import config @@ -24,7 +24,7 @@ class PackageException(Exception): class NoSuchVersionException(PackageException): """Exception indicating that a requested installer version is not available / supported.""" - def __init__(self, package: str = None, version: str = None): + def __init__(self, package: str | None = None, version: str | None = None): message = "Unable to find requested version" if package and version: message += f"Unable to find requested version '{version}' for package '{package}'" @@ -123,6 +123,7 @@ def get_installed_dir(self) -> str | None: directory = self._get_install_dir(target) if directory and os.path.exists(self._get_install_marker_path(directory)): return directory + return None def _get_install_dir(self, target: InstallTarget) -> str: """ @@ -181,7 +182,12 @@ def _post_process(self, target: InstallTarget) -> None: pass -class Package(abc.ABC): +# With Python 3.13 we should be able to set PackageInstaller as the default +# https://typing.python.org/en/latest/spec/generics.html#type-parameter-defaults +T = TypeVar("T", bound=PackageInstaller) + + +class Package(abc.ABC, Generic[T]): """ A Package defines a specific kind of software, mostly used as backends or supporting system for service implementations. @@ -214,7 +220,7 @@ def install(self, version: str | None = None, target: Optional[InstallTarget] = self.get_installer(version).install(target) @functools.lru_cache() - def get_installer(self, version: str | None = None) -> PackageInstaller: + def get_installer(self, version: str | None = None) -> T: """ Returns the installer instance for a specific version of the package. @@ -237,7 +243,7 @@ def get_versions(self) -> List[str]: """ raise NotImplementedError() - def _get_installer(self, version: str) -> PackageInstaller: + def _get_installer(self, version: str) -> T: """ Internal lookup function which needs to be implemented by specific packages. It creates PackageInstaller instances for the specific version. @@ -247,7 +253,7 @@ def _get_installer(self, version: str) -> PackageInstaller: """ raise NotImplementedError() - def __str__(self): + def __str__(self) -> str: return self.name @@ -298,7 +304,7 @@ def _get_install_marker_path(self, install_dir: str) -> str: PLUGIN_NAMESPACE = "localstack.packages" -class PackagesPlugin(Plugin): +class PackagesPlugin(Plugin): # type: ignore[misc] """ Plugin implementation for Package plugins. A package plugin exposes a specific package instance. @@ -311,8 +317,8 @@ def __init__( self, name: str, scope: str, - get_package: Callable[[], Package | List[Package]], - should_load: Callable[[], bool] = None, + get_package: Callable[[], Package[PackageInstaller] | List[Package[PackageInstaller]]], + should_load: Callable[[], bool] | None = None, ) -> None: super().__init__() self.name = name @@ -325,11 +331,11 @@ def should_load(self) -> bool: return self._should_load() return True - def get_package(self) -> Package: + def get_package(self) -> Package[PackageInstaller]: """ :return: returns the package instance of this package plugin """ - return self._get_package() + return self._get_package() # type: ignore[return-value] class NoSuchPackageException(PackageException): @@ -338,20 +344,20 @@ class NoSuchPackageException(PackageException): pass -class PackagesPluginManager(PluginManager[PackagesPlugin]): +class PackagesPluginManager(PluginManager[PackagesPlugin]): # type: ignore[misc] """PluginManager which simplifies the loading / access of PackagesPlugins and their exposed package instances.""" - def __init__(self): + def __init__(self) -> None: super().__init__(PLUGIN_NAMESPACE) - def get_all_packages(self) -> List[Tuple[str, str, Package]]: + def get_all_packages(self) -> list[tuple[str, str, Package[PackageInstaller]]]: return sorted( [(plugin.name, plugin.scope, plugin.get_package()) for plugin in self.load_all()] ) def get_packages( - self, package_names: List[str], version: Optional[str] = None - ) -> List[Package]: + self, package_names: list[str], version: Optional[str] = None + ) -> list[Package[PackageInstaller]]: # Plugin names are unique, but there could be multiple packages with the same name in different scopes plugin_specs_per_name = defaultdict(list) # Plugin names have the format "/", build a dict of specs per package name for the lookup @@ -359,7 +365,7 @@ def get_packages( (package_name, _, _) = plugin_spec.name.rpartition("/") plugin_specs_per_name[package_name].append(plugin_spec) - package_instances: List[Package] = [] + package_instances: list[Package[PackageInstaller]] = [] for package_name in package_names: plugin_specs = plugin_specs_per_name.get(package_name) if not plugin_specs: @@ -377,9 +383,15 @@ def get_packages( return package_instances +P = ParamSpec("P") +T2 = TypeVar("T2") + + def package( - name: str = None, scope: str = "community", should_load: Optional[Callable[[], bool]] = None -): + name: str | None = None, + scope: str = "community", + should_load: Optional[Callable[[], bool]] = None, +) -> Callable[[Callable[[], Package[Any] | list[Package[Any]]]], PluginSpec]: """ Decorator for marking methods that create Package instances as a PackagePlugin. Methods marked with this decorator are discoverable as a PluginSpec within the namespace "localstack.packages", @@ -387,8 +399,8 @@ def package( service name. """ - def wrapper(fn): - _name = name or getmodule(fn).__name__.split(".")[-2] + def wrapper(fn: Callable[[], Package[Any] | list[Package[Any]]]) -> PluginSpec: + _name = name or getmodule(fn).__name__.split(".")[-2] # type: ignore[union-attr] @functools.wraps(fn) def factory() -> PackagesPlugin: diff --git a/localstack-core/localstack/packages/core.py b/localstack-core/localstack/packages/core.py index ae04a4b70f171..5b8996deaa844 100644 --- a/localstack-core/localstack/packages/core.py +++ b/localstack-core/localstack/packages/core.py @@ -4,7 +4,7 @@ from abc import ABC from functools import lru_cache from sys import version_info -from typing import Optional, Tuple +from typing import Any, Optional, Tuple import requests @@ -39,6 +39,7 @@ def get_executable_path(self) -> str | None: install_dir = self.get_installed_dir() if install_dir: return self._get_install_marker_path(install_dir) + return None class DownloadInstaller(ExecutableInstaller): @@ -104,6 +105,7 @@ def get_executable_path(self) -> str | None: if install_dir: install_dir = install_dir[: -len(subdir)] return self._get_install_marker_path(install_dir) + return None def _install(self, target: InstallTarget) -> None: target_directory = self._get_install_dir(target) @@ -133,7 +135,7 @@ def _install(self, target: InstallTarget) -> None: class PermissionDownloadInstaller(DownloadInstaller, ABC): def _install(self, target: InstallTarget) -> None: super()._install(target) - chmod_r(self.get_executable_path(), 0o777) + chmod_r(self.get_executable_path(), 0o777) # type: ignore[arg-type] class GitHubReleaseInstaller(PermissionDownloadInstaller): @@ -249,11 +251,11 @@ class PythonPackageInstaller(PackageInstaller): normalized_name: str """Normalized package name according to PEP440.""" - def __init__(self, name: str, version: str, *args, **kwargs): + def __init__(self, name: str, version: str, *args: Any, **kwargs: Any): super().__init__(name, version, *args, **kwargs) self.normalized_name = self._normalize_package_name(name) - def _normalize_package_name(self, name: str): + def _normalize_package_name(self, name: str) -> str: """ Normalized the Python package name according to PEP440. https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization diff --git a/localstack-core/localstack/packages/debugpy.py b/localstack-core/localstack/packages/debugpy.py index bd2a768b08cd7..2731236f747a1 100644 --- a/localstack-core/localstack/packages/debugpy.py +++ b/localstack-core/localstack/packages/debugpy.py @@ -4,14 +4,14 @@ from localstack.utils.run import run -class DebugPyPackage(Package): - def __init__(self): +class DebugPyPackage(Package["DebugPyPackageInstaller"]): + def __init__(self) -> None: super().__init__("DebugPy", "latest") def get_versions(self) -> List[str]: return ["latest"] - def _get_installer(self, version: str) -> PackageInstaller: + def _get_installer(self, version: str) -> "DebugPyPackageInstaller": return DebugPyPackageInstaller("debugpy", version) @@ -20,7 +20,7 @@ class DebugPyPackageInstaller(PackageInstaller): def is_installed(self) -> bool: try: - import debugpy # noqa: T100 + import debugpy # type: ignore[import-not-found] # noqa: T100 assert debugpy return True diff --git a/localstack-core/localstack/packages/ffmpeg.py b/localstack-core/localstack/packages/ffmpeg.py index 096c4fae34a79..59279701ec81d 100644 --- a/localstack-core/localstack/packages/ffmpeg.py +++ b/localstack-core/localstack/packages/ffmpeg.py @@ -1,7 +1,7 @@ import os from typing import List -from localstack.packages import Package, PackageInstaller +from localstack.packages import Package from localstack.packages.core import ArchiveDownloadAndExtractInstaller from localstack.utils.platform import get_arch @@ -10,11 +10,11 @@ ) -class FfmpegPackage(Package): - def __init__(self): +class FfmpegPackage(Package["FfmpegPackageInstaller"]): + def __init__(self) -> None: super().__init__(name="ffmpeg", default_version="7.0.1") - def _get_installer(self, version: str) -> PackageInstaller: + def _get_installer(self, version: str) -> "FfmpegPackageInstaller": return FfmpegPackageInstaller(version) def get_versions(self) -> List[str]: @@ -35,10 +35,10 @@ def _get_archive_subdir(self) -> str: return f"ffmpeg-{self.version}-{get_arch()}-static" def get_ffmpeg_path(self) -> str: - return os.path.join(self.get_installed_dir(), "ffmpeg") + return os.path.join(self.get_installed_dir(), "ffmpeg") # type: ignore[arg-type] def get_ffprobe_path(self) -> str: - return os.path.join(self.get_installed_dir(), "ffprobe") + return os.path.join(self.get_installed_dir(), "ffprobe") # type: ignore[arg-type] ffmpeg_package = FfmpegPackage() diff --git a/localstack-core/localstack/packages/java.py b/localstack-core/localstack/packages/java.py index c37792ffc011a..c8a2e9f7c7f21 100644 --- a/localstack-core/localstack/packages/java.py +++ b/localstack-core/localstack/packages/java.py @@ -47,8 +47,11 @@ def get_java_lib_path(self) -> str | None: if is_mac_os(): return os.path.join(java_home, "lib", "jli", "libjli.dylib") return os.path.join(java_home, "lib", "server", "libjvm.so") + return None - def get_java_env_vars(self, path: str = None, ld_library_path: str = None) -> dict[str, str]: + def get_java_env_vars( + self, path: str | None = None, ld_library_path: str | None = None + ) -> dict[str, str]: """ Returns environment variables pointing to the Java installation. This is useful to build the environment where the application will run. @@ -64,16 +67,16 @@ def get_java_env_vars(self, path: str = None, ld_library_path: str = None) -> di path = path or os.environ["PATH"] - ld_library_path = ld_library_path or os.environ.get("LD_LIBRARY_PATH") + library_path = ld_library_path or os.environ.get("LD_LIBRARY_PATH") # null paths (e.g. `:/foo`) have a special meaning according to the manpages - if ld_library_path is None: - ld_library_path = f"{java_home}/lib:{java_home}/lib/server" + if library_path is None: + full_library_path = f"{java_home}/lib:{java_home}/lib/server" else: - ld_library_path = f"{java_home}/lib:{java_home}/lib/server:{ld_library_path}" + full_library_path = f"{java_home}/lib:{java_home}/lib/server:{library_path}" return { - "JAVA_HOME": java_home, - "LD_LIBRARY_PATH": ld_library_path, + "JAVA_HOME": java_home, # type: ignore[dict-item] + "LD_LIBRARY_PATH": full_library_path, "PATH": f"{java_bin}:{path}", } @@ -144,7 +147,7 @@ def get_java_home(self) -> str | None: """ installed_dir = self.get_installed_dir() if is_mac_os(): - return os.path.join(installed_dir, "Contents", "Home") + return os.path.join(installed_dir, "Contents", "Home") # type: ignore[arg-type] return installed_dir @property @@ -188,14 +191,14 @@ def _download_url_fallback(self) -> str: ) -class JavaPackage(Package): +class JavaPackage(Package[JavaPackageInstaller]): def __init__(self, default_version: str = DEFAULT_JAVA_VERSION): super().__init__(name="Java", default_version=default_version) def get_versions(self) -> List[str]: return list(JAVA_VERSIONS.keys()) - def _get_installer(self, version): + def _get_installer(self, version: str) -> JavaPackageInstaller: return JavaPackageInstaller(version) diff --git a/localstack-core/localstack/packages/plugins.py b/localstack-core/localstack/packages/plugins.py index 4b4b200af8e0c..fdeba86a04204 100644 --- a/localstack-core/localstack/packages/plugins.py +++ b/localstack-core/localstack/packages/plugins.py @@ -1,22 +1,29 @@ +from typing import TYPE_CHECKING + from localstack.packages.api import Package, package +if TYPE_CHECKING: + from localstack.packages.ffmpeg import FfmpegPackageInstaller + from localstack.packages.java import JavaPackageInstaller + from localstack.packages.terraform import TerraformPackageInstaller + @package(name="terraform") -def terraform_package() -> Package: +def terraform_package() -> Package["TerraformPackageInstaller"]: from .terraform import terraform_package return terraform_package @package(name="ffmpeg") -def ffmpeg_package() -> Package: +def ffmpeg_package() -> Package["FfmpegPackageInstaller"]: from localstack.packages.ffmpeg import ffmpeg_package return ffmpeg_package @package(name="java") -def java_package() -> Package: +def java_package() -> Package["JavaPackageInstaller"]: from localstack.packages.java import java_package return java_package diff --git a/localstack-core/localstack/packages/terraform.py b/localstack-core/localstack/packages/terraform.py index 703380c54c07e..6ee590f0387b5 100644 --- a/localstack-core/localstack/packages/terraform.py +++ b/localstack-core/localstack/packages/terraform.py @@ -2,7 +2,7 @@ import platform from typing import List -from localstack.packages import InstallTarget, Package, PackageInstaller +from localstack.packages import InstallTarget, Package from localstack.packages.core import ArchiveDownloadAndExtractInstaller from localstack.utils.files import chmod_r from localstack.utils.platform import get_arch @@ -13,14 +13,14 @@ ) -class TerraformPackage(Package): - def __init__(self): +class TerraformPackage(Package["TerraformPackageInstaller"]): + def __init__(self) -> None: super().__init__("Terraform", TERRAFORM_VERSION) def get_versions(self) -> List[str]: return [TERRAFORM_VERSION] - def _get_installer(self, version: str) -> PackageInstaller: + def _get_installer(self, version: str) -> "TerraformPackageInstaller": return TerraformPackageInstaller("terraform", version) @@ -35,7 +35,7 @@ def _get_download_url(self) -> str: def _install(self, target: InstallTarget) -> None: super()._install(target) - chmod_r(self.get_executable_path(), 0o777) + chmod_r(self.get_executable_path(), 0o777) # type: ignore[arg-type] terraform_package = TerraformPackage() diff --git a/localstack-core/localstack/services/kinesis/packages.py b/localstack-core/localstack/services/kinesis/packages.py index 53ef6b7c53610..d6b68dcd9d628 100644 --- a/localstack-core/localstack/services/kinesis/packages.py +++ b/localstack-core/localstack/services/kinesis/packages.py @@ -2,18 +2,18 @@ from functools import lru_cache from typing import List -from localstack.packages import Package, PackageInstaller +from localstack.packages import Package from localstack.packages.core import NodePackageInstaller _KINESIS_MOCK_VERSION = os.environ.get("KINESIS_MOCK_VERSION") or "0.4.9" -class KinesisMockPackage(Package): +class KinesisMockPackage(Package[NodePackageInstaller]): def __init__(self, default_version: str = _KINESIS_MOCK_VERSION): super().__init__(name="Kinesis Mock", default_version=default_version) @lru_cache - def _get_installer(self, version: str) -> PackageInstaller: + def _get_installer(self, version: str) -> NodePackageInstaller: return KinesisMockPackageInstaller(version) def get_versions(self) -> List[str]: diff --git a/localstack-core/localstack/utils/archives.py b/localstack-core/localstack/utils/archives.py index dfba8d3c9aafc..97477f6d86c74 100644 --- a/localstack-core/localstack/utils/archives.py +++ b/localstack-core/localstack/utils/archives.py @@ -1,21 +1,14 @@ -import io -import tarfile -import zipfile -from subprocess import Popen -from typing import IO, Optional - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - import glob +import io import logging import os import re +import tarfile import tempfile import time -from typing import Union +import zipfile +from subprocess import Popen +from typing import IO, Literal, Optional, Union from localstack.constants import MAVEN_REPO_URL from localstack.utils.files import load_file, mkdir, new_tmp_file, rm_rf, save_file @@ -177,7 +170,13 @@ def upgrade_jar_file(base_dir: str, file_glob: str, maven_asset: str): download(maven_asset_url, target_file) -def download_and_extract(archive_url, target_dir, retries=0, sleep=3, tmp_archive=None): +def download_and_extract( + archive_url: str, + target_dir: str, + retries: Optional[int] = 0, + sleep: Optional[int] = 3, + tmp_archive: Optional[str] = None, +) -> None: mkdir(target_dir) _, ext = os.path.splitext(tmp_archive or archive_url) diff --git a/localstack-core/localstack/utils/venv.py b/localstack-core/localstack/utils/venv.py index 21d5bf4fa3ece..7911110ce54f6 100644 --- a/localstack-core/localstack/utils/venv.py +++ b/localstack-core/localstack/utils/venv.py @@ -14,7 +14,7 @@ class VirtualEnvironment: def __init__(self, venv_dir: Union[str, os.PathLike]): self._venv_dir = venv_dir - def create(self): + def create(self) -> None: """ Uses the virtualenv cli to create the virtual environment. :return: @@ -73,7 +73,7 @@ def site_dir(self) -> Path: return matches[0] - def inject_to_sys_path(self): + def inject_to_sys_path(self) -> None: path = str(self.site_dir) if path and path not in sys.path: sys.path.append(path) diff --git a/localstack-core/mypy.ini b/localstack-core/mypy.ini new file mode 100644 index 0000000000000..d5ec889accc0b --- /dev/null +++ b/localstack-core/mypy.ini @@ -0,0 +1,19 @@ +[mypy] +explicit_package_bases = true +mypy_path=localstack-core +files=localstack/packages,localstack/services/kinesis/packages.py +ignore_missing_imports = False +follow_imports = silent +ignore_errors = False +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_any_generics = True +disallow_subclassing_any = True +warn_unused_ignores = True + +[mypy-localstack.services.lambda_.invocation.*,localstack.services.lambda_.provider] +ignore_errors = False +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_any_generics = True +allow_untyped_globals = False diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index f53f61c32fa73..0000000000000 --- a/mypy.ini +++ /dev/null @@ -1,10 +0,0 @@ -[mypy] -ignore_missing_imports = True -ignore_errors = True - -[mypy-localstack.services.lambda_.invocation.*,localstack.services.lambda_.provider] -ignore_errors = False -disallow_untyped_defs = True -disallow_untyped_calls = True -disallow_any_generics = True -allow_untyped_globals = False diff --git a/pyproject.toml b/pyproject.toml index cc1a5be68b614..7b2a573c685e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ dev = [ "pypandoc", "ruff>=0.3.3", "rstr>=3.2.0", + "mypy", ] # not strictly necessary for development, but provides type hint support for a better developer experience diff --git a/requirements-dev.txt b/requirements-dev.txt index 82d230cb8e48a..33448857b81b2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -256,6 +256,10 @@ mpmath==1.3.0 # via sympy multipart==1.2.1 # via moto-ext +mypy==1.15.0 + # via localstack-core (pyproject.toml) +mypy-extensions==1.0.0 + # via mypy networkx==3.4.2 # via # cfn-lint @@ -464,6 +468,7 @@ typing-extensions==4.13.2 # cfn-lint # jsii # localstack-twisted + # mypy # pydantic # pydantic-core # pyopenssl diff --git a/requirements-typehint.txt b/requirements-typehint.txt index c919ac8a799af..6c30ea635abc9 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -260,6 +260,8 @@ mpmath==1.3.0 # via sympy multipart==1.2.1 # via moto-ext +mypy==1.15.0 + # via localstack-core mypy-boto3-acm==1.37.0 # via boto3-stubs mypy-boto3-acm-pca==1.37.12 @@ -466,6 +468,8 @@ mypy-boto3-wafv2==1.37.21 # via boto3-stubs mypy-boto3-xray==1.37.0 # via boto3-stubs +mypy-extensions==1.0.0 + # via mypy networkx==3.4.2 # via # cfn-lint @@ -675,113 +679,10 @@ typing-extensions==4.13.2 # via # anyio # aws-sam-translator - # boto3-stubs # cfn-lint # jsii # localstack-twisted - # mypy-boto3-acm - # mypy-boto3-acm-pca - # mypy-boto3-amplify - # mypy-boto3-apigateway - # mypy-boto3-apigatewayv2 - # mypy-boto3-appconfig - # mypy-boto3-appconfigdata - # mypy-boto3-application-autoscaling - # mypy-boto3-appsync - # mypy-boto3-athena - # mypy-boto3-autoscaling - # mypy-boto3-backup - # mypy-boto3-batch - # mypy-boto3-ce - # mypy-boto3-cloudcontrol - # mypy-boto3-cloudformation - # mypy-boto3-cloudfront - # mypy-boto3-cloudtrail - # mypy-boto3-cloudwatch - # mypy-boto3-codebuild - # mypy-boto3-codecommit - # mypy-boto3-codeconnections - # mypy-boto3-codedeploy - # mypy-boto3-codepipeline - # mypy-boto3-codestar-connections - # mypy-boto3-cognito-identity - # mypy-boto3-cognito-idp - # mypy-boto3-dms - # mypy-boto3-docdb - # mypy-boto3-dynamodb - # mypy-boto3-dynamodbstreams - # mypy-boto3-ec2 - # mypy-boto3-ecr - # mypy-boto3-ecs - # mypy-boto3-efs - # mypy-boto3-eks - # mypy-boto3-elasticache - # mypy-boto3-elasticbeanstalk - # mypy-boto3-elbv2 - # mypy-boto3-emr - # mypy-boto3-emr-serverless - # mypy-boto3-es - # mypy-boto3-events - # mypy-boto3-firehose - # mypy-boto3-fis - # mypy-boto3-glacier - # mypy-boto3-glue - # mypy-boto3-iam - # mypy-boto3-identitystore - # mypy-boto3-iot - # mypy-boto3-iot-data - # mypy-boto3-iotanalytics - # mypy-boto3-iotwireless - # mypy-boto3-kafka - # mypy-boto3-kinesis - # mypy-boto3-kinesisanalytics - # mypy-boto3-kinesisanalyticsv2 - # mypy-boto3-kms - # mypy-boto3-lakeformation - # mypy-boto3-lambda - # mypy-boto3-logs - # mypy-boto3-managedblockchain - # mypy-boto3-mediaconvert - # mypy-boto3-mediastore - # mypy-boto3-mq - # mypy-boto3-mwaa - # mypy-boto3-neptune - # mypy-boto3-opensearch - # mypy-boto3-organizations - # mypy-boto3-pi - # mypy-boto3-pinpoint - # mypy-boto3-pipes - # mypy-boto3-qldb - # mypy-boto3-qldb-session - # mypy-boto3-rds - # mypy-boto3-rds-data - # mypy-boto3-redshift - # mypy-boto3-redshift-data - # mypy-boto3-resource-groups - # mypy-boto3-resourcegroupstaggingapi - # mypy-boto3-route53 - # mypy-boto3-route53resolver - # mypy-boto3-s3 - # mypy-boto3-s3control - # mypy-boto3-sagemaker - # mypy-boto3-sagemaker-runtime - # mypy-boto3-secretsmanager - # mypy-boto3-serverlessrepo - # mypy-boto3-servicediscovery - # mypy-boto3-ses - # mypy-boto3-sesv2 - # mypy-boto3-sns - # mypy-boto3-sqs - # mypy-boto3-ssm - # mypy-boto3-sso-admin - # mypy-boto3-stepfunctions - # mypy-boto3-sts - # mypy-boto3-timestream-query - # mypy-boto3-timestream-write - # mypy-boto3-transcribe - # mypy-boto3-verifiedpermissions - # mypy-boto3-wafv2 - # mypy-boto3-xray + # mypy # pydantic # pydantic-core # pyopenssl From 4ea410231ddd2ebe6c2c1cb33a16fcb29bafe9ca Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:28:24 +0200 Subject: [PATCH 068/108] Upgrade pinned Python dependencies (#12544) Co-authored-by: LocalStack Bot --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 6 +++--- requirements-basic.txt | 2 +- requirements-dev.txt | 16 ++++++++-------- requirements-runtime.txt | 8 ++++---- requirements-test.txt | 12 ++++++------ requirements-typehint.txt | 36 +++++++++++++++++------------------ 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c33108fb7cdb8..1ffd7c5259913 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.5 + rev: v0.11.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index ec58c4c368b22..de16eb6f3fbc4 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -92,7 +92,7 @@ jsonschema-specifications==2024.10.1 # via # jsonschema # openapi-schema-validator -lazy-object-proxy==1.10.0 +lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-twisted==24.3.0 # via localstack-core (pyproject.toml) @@ -112,7 +112,7 @@ openapi-schema-validator==0.6.3 # openapi-spec-validator openapi-spec-validator==0.7.1 # via openapi-core -packaging==24.2 +packaging==25.0 # via build parse==1.20.2 # via openapi-core @@ -170,7 +170,7 @@ rpds-py==0.24.0 # via # jsonschema # referencing -s3transfer==0.11.4 +s3transfer==0.11.5 # via boto3 semver==3.0.4 # via localstack-core (pyproject.toml) diff --git a/requirements-basic.txt b/requirements-basic.txt index afefd552e94a1..a37daec3d90db 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -30,7 +30,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -packaging==24.2 +packaging==25.0 # via build plux==1.12.1 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 33448857b81b2..ebaa57577d5f8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.232 +aws-cdk-asset-awscli-v1==2.2.233 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.189.1 +aws-cdk-lib==2.190.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -82,7 +82,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.33.2 +cfn-lint==1.34.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -170,7 +170,7 @@ hyperframe==6.1.0 # via h2 hyperlink==21.0.0 # via localstack-twisted -identify==2.6.9 +identify==2.6.10 # via pre-commit idna==3.10 # via @@ -234,7 +234,7 @@ jsonschema-specifications==2024.10.1 # via # jsonschema # openapi-schema-validator -lazy-object-proxy==1.10.0 +lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 # via localstack-core @@ -281,7 +281,7 @@ opensearch-py==2.8.0 # via localstack-core orderly-set==5.4.0 # via deepdiff -packaging==24.2 +packaging==25.0 # via # apispec # build @@ -429,9 +429,9 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.11.5 +ruff==0.11.6 # via localstack-core (pyproject.toml) -s3transfer==0.11.4 +s3transfer==0.11.5 # via # awscli # boto3 diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 378165d67c158..f565f23b99873 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -64,7 +64,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.33.2 +cfn-lint==1.34.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -174,7 +174,7 @@ jsonschema-specifications==2024.10.1 # via # jsonschema # openapi-schema-validator -lazy-object-proxy==1.10.0 +lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-twisted==24.3.0 # via localstack-core @@ -208,7 +208,7 @@ openapi-spec-validator==0.7.1 # openapi-core opensearch-py==2.8.0 # via localstack-core (pyproject.toml) -packaging==24.2 +packaging==25.0 # via # apispec # build @@ -312,7 +312,7 @@ rpds-py==0.24.0 # referencing rsa==4.7.2 # via awscli -s3transfer==0.11.4 +s3transfer==0.11.5 # via # awscli # boto3 diff --git a/requirements-test.txt b/requirements-test.txt index 67715c62c9c7d..e1d422607182f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.232 +aws-cdk-asset-awscli-v1==2.2.233 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.189.1 +aws-cdk-lib==2.190.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.97.0 # via @@ -80,7 +80,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.33.2 +cfn-lint==1.34.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -218,7 +218,7 @@ jsonschema-specifications==2024.10.1 # via # jsonschema # openapi-schema-validator -lazy-object-proxy==1.10.0 +lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 # via localstack-core (pyproject.toml) @@ -256,7 +256,7 @@ opensearch-py==2.8.0 # via localstack-core orderly-set==5.4.0 # via deepdiff -packaging==24.2 +packaging==25.0 # via # apispec # build @@ -389,7 +389,7 @@ rpds-py==0.24.0 # referencing rsa==4.7.2 # via awscli -s3transfer==0.11.4 +s3transfer==0.11.5 # via # awscli # boto3 diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 6c30ea635abc9..1ceea72941f1f 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -27,13 +27,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.232 +aws-cdk-asset-awscli-v1==2.2.233 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.189.1 +aws-cdk-lib==2.190.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -51,7 +51,7 @@ boto3==1.37.28 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.37.34 +boto3-stubs==1.37.38 # via localstack-core (pyproject.toml) botocore==1.37.28 # via @@ -61,7 +61,7 @@ botocore==1.37.28 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.37.29 +botocore-stubs==1.37.38 # via boto3-stubs build==1.2.2.post1 # via @@ -86,7 +86,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.33.2 +cfn-lint==1.34.1 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -174,7 +174,7 @@ hyperframe==6.1.0 # via h2 hyperlink==21.0.0 # via localstack-twisted -identify==2.6.9 +identify==2.6.10 # via pre-commit idna==3.10 # via @@ -238,7 +238,7 @@ jsonschema-specifications==2024.10.1 # via # jsonschema # openapi-schema-validator -lazy-object-proxy==1.10.0 +lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 # via localstack-core @@ -282,7 +282,7 @@ mypy-boto3-appsync==1.37.15 # via boto3-stubs mypy-boto3-athena==1.37.0 # via boto3-stubs -mypy-boto3-autoscaling==1.37.0 +mypy-boto3-autoscaling==1.37.36 # via boto3-stubs mypy-boto3-backup==1.37.0 # via boto3-stubs @@ -328,11 +328,11 @@ mypy-boto3-ec2==1.37.28 # via boto3-stubs mypy-boto3-ecr==1.37.26 # via boto3-stubs -mypy-boto3-ecs==1.37.26 +mypy-boto3-ecs==1.37.36 # via boto3-stubs mypy-boto3-efs==1.37.0 # via boto3-stubs -mypy-boto3-eks==1.37.24 +mypy-boto3-eks==1.37.35 # via boto3-stubs mypy-boto3-elasticache==1.37.32 # via boto3-stubs @@ -346,9 +346,9 @@ mypy-boto3-emr-serverless==1.37.0 # via boto3-stubs mypy-boto3-es==1.37.0 # via boto3-stubs -mypy-boto3-events==1.37.28 +mypy-boto3-events==1.37.35 # via boto3-stubs -mypy-boto3-firehose==1.37.0 +mypy-boto3-firehose==1.37.38 # via boto3-stubs mypy-boto3-fis==1.37.0 # via boto3-stubs @@ -418,7 +418,7 @@ mypy-boto3-redshift==1.37.0 # via boto3-stubs mypy-boto3-redshift-data==1.37.8 # via boto3-stubs -mypy-boto3-resource-groups==1.37.0 +mypy-boto3-resource-groups==1.37.35 # via boto3-stubs mypy-boto3-resourcegroupstaggingapi==1.37.0 # via boto3-stubs @@ -430,7 +430,7 @@ mypy-boto3-s3==1.37.24 # via boto3-stubs mypy-boto3-s3control==1.37.28 # via boto3-stubs -mypy-boto3-sagemaker==1.37.27 +mypy-boto3-sagemaker==1.37.37 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.37.0 # via boto3-stubs @@ -491,7 +491,7 @@ opensearch-py==2.8.0 # via localstack-core orderly-set==5.4.0 # via deepdiff -packaging==24.2 +packaging==25.0 # via # apispec # build @@ -639,9 +639,9 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.11.5 +ruff==0.11.6 # via localstack-core -s3transfer==0.11.4 +s3transfer==0.11.5 # via # awscli # boto3 @@ -673,7 +673,7 @@ typeguard==2.13.3 # jsii types-awscrt==0.26.1 # via botocore-stubs -types-s3transfer==0.11.4 +types-s3transfer==0.11.5 # via boto3-stubs typing-extensions==4.13.2 # via From 7e1e8991a93b7a24f6ccf98d90db179606e1a7e7 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:28:45 +0200 Subject: [PATCH 069/108] APIGW: validate REST API custom id tag (#12539) --- .../services/apigateway/next_gen/provider.py | 14 ++++++++++++++ .../aws/services/apigateway/test_apigateway_api.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/services/apigateway/next_gen/provider.py b/localstack-core/localstack/services/apigateway/next_gen/provider.py index 9c3dab33bfe86..43b05da4ddc3c 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/provider.py +++ b/localstack-core/localstack/services/apigateway/next_gen/provider.py @@ -1,6 +1,8 @@ from localstack.aws.api import CommonServiceException, RequestContext, handler from localstack.aws.api.apigateway import ( + BadRequestException, CacheClusterSize, + CreateRestApiRequest, CreateStageRequest, Deployment, DeploymentCanarySettings, @@ -12,12 +14,14 @@ NotFoundException, NullableBoolean, NullableInteger, + RestApi, Stage, StatusCode, String, TestInvokeMethodRequest, TestInvokeMethodResponse, ) +from localstack.constants import TAG_KEY_CUSTOM_ID from localstack.services.apigateway.helpers import ( get_apigateway_store, get_moto_rest_api, @@ -56,6 +60,16 @@ def on_after_init(self): apply_patches() self.router.register_routes() + @handler("CreateRestApi", expand=False) + def create_rest_api(self, context: RequestContext, request: CreateRestApiRequest) -> RestApi: + if "-" in request.get("tags", {}).get(TAG_KEY_CUSTOM_ID, ""): + raise BadRequestException( + f"The '{TAG_KEY_CUSTOM_ID}' tag cannot contain the '-' character." + ) + + response = super().create_rest_api(context, request) + return response + @handler("DeleteRestApi") def delete_rest_api(self, context: RequestContext, rest_api_id: String, **kwargs) -> None: super().delete_rest_api(context, rest_api_id, **kwargs) diff --git a/tests/aws/services/apigateway/test_apigateway_api.py b/tests/aws/services/apigateway/test_apigateway_api.py index 686df0ba88a85..612c956bbbd1c 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.py +++ b/tests/aws/services/apigateway/test_apigateway_api.py @@ -201,7 +201,7 @@ def test_create_rest_api_with_tags(self, apigw_create_rest_api, snapshot, aws_cl snapshot.match("get-rest-apis-w-tags", response) @markers.aws.only_localstack - def test_create_rest_api_with_custom_id_tag(self, apigw_create_rest_api): + def test_create_rest_api_with_custom_id_tag(self, apigw_create_rest_api, aws_client): custom_id_tag = "testid123" response = apigw_create_rest_api( name="my_api", description="this is my api", tags={TAG_KEY_CUSTOM_ID: custom_id_tag} @@ -209,6 +209,13 @@ def test_create_rest_api_with_custom_id_tag(self, apigw_create_rest_api): api_id = response["id"] assert api_id == custom_id_tag + with pytest.raises(aws_client.apigateway.exceptions.BadRequestException): + apigw_create_rest_api( + name="my_api", + description="bad custom id", + tags={TAG_KEY_CUSTOM_ID: "bad-custom-id-hyphen"}, + ) + @markers.aws.validated def test_update_rest_api_operation_add_remove( self, apigw_create_rest_api, snapshot, aws_client From 77f30dcd2ed68017e4972d81d5f69a86bcab2b5f Mon Sep 17 00:00:00 2001 From: Maciej Pakulski Date: Wed, 23 Apr 2025 09:50:15 +0200 Subject: [PATCH 070/108] Add dry_run support to GenerateDataKeyPair/GenerateDataKeyPairWithoutPlaintext (#12505) --- .../localstack/services/kms/models.py | 52 ++++- .../localstack/services/kms/provider.py | 27 ++- .../localstack/services/kms/utils.py | 31 ++- tests/aws/services/kms/test_kms.py | 42 ++++ tests/aws/services/kms/test_kms.snapshot.json | 32 +++ .../aws/services/kms/test_kms.validation.json | 6 + tests/unit/services/kms/test_kms.py | 200 +++++++++++++++++- 7 files changed, 371 insertions(+), 19 deletions(-) diff --git a/localstack-core/localstack/services/kms/models.py b/localstack-core/localstack/services/kms/models.py index 6f91ada1c923d..3479e309d4903 100644 --- a/localstack-core/localstack/services/kms/models.py +++ b/localstack-core/localstack/services/kms/models.py @@ -178,6 +178,45 @@ class KmsCryptoKey: key_material: bytes key_spec: str + @staticmethod + def assert_valid(key_spec: str): + """ + Validates that the given ``key_spec`` is supported in the current context. + + :param key_spec: The key specification to validate. + :type key_spec: str + :raises ValidationException: If ``key_spec`` is not a known valid spec. + :raises UnsupportedOperationException: If ``key_spec`` is entirely unsupported. + """ + + def raise_validation(): + raise ValidationException( + f"1 validation error detected: Value '{key_spec}' at 'keySpec' " + f"failed to satisfy constraint: Member must satisfy enum value set: " + f"[RSA_2048, ECC_NIST_P384, ECC_NIST_P256, ECC_NIST_P521, HMAC_384, RSA_3072, " + f"ECC_SECG_P256K1, RSA_4096, SYMMETRIC_DEFAULT, HMAC_256, HMAC_224, HMAC_512]" + ) + + if key_spec == "SYMMETRIC_DEFAULT": + return + + if key_spec.startswith("RSA"): + if key_spec not in RSA_CRYPTO_KEY_LENGTHS: + raise_validation() + return + + if key_spec.startswith("ECC"): + if key_spec not in ECC_CURVES: + raise_validation() + return + + if key_spec.startswith("HMAC"): + if key_spec not in HMAC_RANGE_KEY_LENGTHS: + raise_validation() + return + + raise UnsupportedOperationException(f"KeySpec {key_spec} is not supported") + def __init__(self, key_spec: str, key_material: Optional[bytes] = None): self.private_key = None self.public_key = None @@ -188,6 +227,8 @@ def __init__(self, key_spec: str, key_material: Optional[bytes] = None): self.key_material = key_material or os.urandom(SYMMETRIC_DEFAULT_MATERIAL_LENGTH) self.key_spec = key_spec + KmsCryptoKey.assert_valid(key_spec) + if key_spec == "SYMMETRIC_DEFAULT": return @@ -201,22 +242,11 @@ def __init__(self, key_spec: str, key_material: Optional[bytes] = None): else: key = ec.generate_private_key(curve) elif key_spec.startswith("HMAC"): - if key_spec not in HMAC_RANGE_KEY_LENGTHS: - raise ValidationException( - f"1 validation error detected: Value '{key_spec}' at 'keySpec' " - f"failed to satisfy constraint: Member must satisfy enum value set: " - f"[RSA_2048, ECC_NIST_P384, ECC_NIST_P256, ECC_NIST_P521, HMAC_384, RSA_3072, " - f"ECC_SECG_P256K1, RSA_4096, SYMMETRIC_DEFAULT, HMAC_256, HMAC_224, HMAC_512]" - ) minimum_length, maximum_length = HMAC_RANGE_KEY_LENGTHS.get(key_spec) self.key_material = key_material or os.urandom( random.randint(minimum_length, maximum_length) ) return - else: - # We do not support SM2 - asymmetric keys both suitable for ENCRYPT_DECRYPT and SIGN_VERIFY, - # but only used in China AWS regions. - raise UnsupportedOperationException(f"KeySpec {key_spec} is not supported") self._serialize_key(key) diff --git a/localstack-core/localstack/services/kms/provider.py b/localstack-core/localstack/services/kms/provider.py index cb285626a092c..9f29780fa2103 100644 --- a/localstack-core/localstack/services/kms/provider.py +++ b/localstack-core/localstack/services/kms/provider.py @@ -123,7 +123,12 @@ deserialize_ciphertext_blob, kms_stores, ) -from localstack.services.kms.utils import is_valid_key_arn, parse_key_arn, validate_alias_name +from localstack.services.kms.utils import ( + execute_dry_run_capable, + is_valid_key_arn, + parse_key_arn, + validate_alias_name, +) from localstack.services.plugins import ServiceLifecycleHook from localstack.utils.aws.arns import get_partition, kms_alias_arn, parse_arn from localstack.utils.collections import PaginatedList @@ -732,11 +737,21 @@ def _generate_data_key_pair( key_id: str, key_pair_spec: str, encryption_context: EncryptionContextType = None, + dry_run: NullableBooleanType = None, ): account_id, region_name, key_id = self._parse_key_id(key_id, context) key = self._get_kms_key(account_id, region_name, key_id) self._validate_key_for_encryption_decryption(context, key) + KmsCryptoKey.assert_valid(key_pair_spec) + return execute_dry_run_capable( + self._build_data_key_pair_response, dry_run, key, key_pair_spec, encryption_context + ) + + def _build_data_key_pair_response( + self, key: KmsKey, key_pair_spec: str, encryption_context: EncryptionContextType = None + ): crypto_key = KmsCryptoKey(key_pair_spec) + return { "KeyId": key.metadata["Arn"], "KeyPairSpec": key_pair_spec, @@ -757,8 +772,9 @@ def generate_data_key_pair( dry_run: NullableBooleanType = None, **kwargs, ) -> GenerateDataKeyPairResponse: - # TODO add support for "dry_run" - result = self._generate_data_key_pair(context, key_id, key_pair_spec, encryption_context) + result = self._generate_data_key_pair( + context, key_id, key_pair_spec, encryption_context, dry_run + ) return GenerateDataKeyPairResponse(**result) @handler("GenerateRandom", expand=False) @@ -794,8 +810,9 @@ def generate_data_key_pair_without_plaintext( dry_run: NullableBooleanType = None, **kwargs, ) -> GenerateDataKeyPairWithoutPlaintextResponse: - # TODO add support for "dry_run" - result = self._generate_data_key_pair(context, key_id, key_pair_spec, encryption_context) + result = self._generate_data_key_pair( + context, key_id, key_pair_spec, encryption_context, dry_run + ) result.pop("PrivateKeyPlaintext") return GenerateDataKeyPairResponse(**result) diff --git a/localstack-core/localstack/services/kms/utils.py b/localstack-core/localstack/services/kms/utils.py index ce1a65599e6c8..ae9ff4580caa1 100644 --- a/localstack-core/localstack/services/kms/utils.py +++ b/localstack-core/localstack/services/kms/utils.py @@ -1,10 +1,12 @@ import re -from typing import Tuple +from typing import Callable, Tuple, TypeVar -from localstack.aws.api.kms import Tag, TagException +from localstack.aws.api.kms import DryRunOperationException, Tag, TagException from localstack.services.kms.exceptions import ValidationException from localstack.utils.aws.arns import ARN_PARTITION_REGEX +T = TypeVar("T") + KMS_KEY_ARN_PATTERN = re.compile( rf"{ARN_PARTITION_REGEX}:kms:(?P[^:]+):(?P\d{{12}}):key\/(?P[^:]+)$" ) @@ -58,3 +60,28 @@ def validate_tag(tag_position: int, tag: Tag) -> None: if tag_key.lower().startswith("aws:"): raise TagException("Tags beginning with aws: are reserved") + + +def execute_dry_run_capable(func: Callable[..., T], dry_run: bool, *args, **kwargs) -> T: + """ + Executes a function unless dry run mode is enabled. + + If ``dry_run`` is ``True``, the function is not executed and a + ``DryRunOperationException`` is raised. Otherwise, the provided + function is called with the given positional and keyword arguments. + + :param func: The function to be executed. + :type func: Callable[..., T] + :param dry_run: Flag indicating whether the execution is a dry run. + :type dry_run: bool + :param args: Positional arguments to pass to the function. + :param kwargs: Keyword arguments to pass to the function. + :returns: The result of the function call if ``dry_run`` is ``False``. + :rtype: T + :raises DryRunOperationException: If ``dry_run`` is ``True``. + """ + if dry_run: + raise DryRunOperationException( + "The request would have succeeded, but the DryRun option is set." + ) + return func(*args, **kwargs) diff --git a/tests/aws/services/kms/test_kms.py b/tests/aws/services/kms/test_kms.py index 23b52722e6326..4b68dd9c38dce 100644 --- a/tests/aws/services/kms/test_kms.py +++ b/tests/aws/services/kms/test_kms.py @@ -2051,3 +2051,45 @@ def test_encryption_context_generate_data_key_pair_without_plaintext( with pytest.raises(ClientError) as e: aws_client.kms.decrypt(CiphertextBlob=result["PrivateKeyCiphertextBlob"], KeyId=key_id) snapshot.match("decrypt-without-encryption-context", e.value.response) + + @markers.aws.validated + def test_generate_data_key_pair_dry_run(self, kms_key, aws_client, snapshot): + snapshot.add_transformer( + snapshot.transform.key_value("PrivateKeyCiphertextBlob", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("PrivateKeyPlaintext", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("PublicKey", reference_replacement=False) + ) + + key_id = kms_key["KeyId"] + + with pytest.raises(ClientError) as exc: + aws_client.kms.generate_data_key_pair(KeyId=key_id, KeyPairSpec="RSA_2048", DryRun=True) + + err = exc.value.response + snapshot.match("dryrun_exception", err) + + @markers.aws.validated + def test_generate_data_key_pair_without_plaintext_dry_run(self, kms_key, aws_client, snapshot): + snapshot.add_transformer( + snapshot.transform.key_value("PrivateKeyCiphertextBlob", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("PublicKey", reference_replacement=False) + ) + + key_id = kms_key["KeyId"] + aws_client.kms.generate_data_key_pair_without_plaintext( + KeyId=key_id, KeyPairSpec="RSA_2048" + ) + + with pytest.raises(ClientError) as exc: + aws_client.kms.generate_data_key_pair_without_plaintext( + KeyId=key_id, KeyPairSpec="RSA_2048", DryRun=True + ) + + err = exc.value.response + snapshot.match("dryrun_exception", err) diff --git a/tests/aws/services/kms/test_kms.snapshot.json b/tests/aws/services/kms/test_kms.snapshot.json index fea96abd3ab31..17ebf79f26bb7 100644 --- a/tests/aws/services/kms/test_kms.snapshot.json +++ b/tests/aws/services/kms/test_kms.snapshot.json @@ -2206,5 +2206,37 @@ } } } + }, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_dry_run": { + "recorded-date": "06-04-2025, 11:54:20", + "recorded-content": { + "dryrun_exception": { + "Error": { + "Code": "DryRunOperationException", + "Message": "The request would have succeeded, but the DryRun option is set." + }, + "message": "The request would have succeeded, but the DryRun option is set.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_without_plaintext_dry_run": { + "recorded-date": "07-04-2025, 17:12:37", + "recorded-content": { + "dryrun_exception": { + "Error": { + "Code": "DryRunOperationException", + "Message": "The request would have succeeded, but the DryRun option is set." + }, + "message": "The request would have succeeded, but the DryRun option is set.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/kms/test_kms.validation.json b/tests/aws/services/kms/test_kms.validation.json index 419b36f95854f..fb082e9a3265d 100644 --- a/tests/aws/services/kms/test_kms.validation.json +++ b/tests/aws/services/kms/test_kms.validation.json @@ -328,5 +328,11 @@ }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_without_plaintext": { "last_validated_date": "2024-04-11T15:54:31+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_dry_run": { + "last_validated_date": "2025-04-06T11:54:20+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_without_plaintext_dry_run": { + "last_validated_date": "2025-04-13T15:44:57+00:00" } } diff --git a/tests/unit/services/kms/test_kms.py b/tests/unit/services/kms/test_kms.py index 109b8d09630ec..72e4b870eb36c 100644 --- a/tests/unit/services/kms/test_kms.py +++ b/tests/unit/services/kms/test_kms.py @@ -1,8 +1,206 @@ import pytest -from localstack.services.kms.utils import validate_alias_name +from localstack.aws.api import RequestContext +from localstack.aws.api.kms import ( + CreateKeyRequest, + DryRunOperationException, + UnsupportedOperationException, +) +from localstack.services.kms.exceptions import ValidationException +from localstack.services.kms.provider import KmsProvider +from localstack.services.kms.utils import ( + execute_dry_run_capable, + validate_alias_name, +) def test_alias_name_validator(): with pytest.raises(Exception): validate_alias_name("test-alias") + + +@pytest.fixture +def provider(): + return KmsProvider() + + +def test_execute_dry_run_capable_runs_when_not_dry(): + result = execute_dry_run_capable(lambda: 1 + 1, dry_run=False) + assert result == 2 + + +def test_execute_dry_run_capable_raises_when_dry(): + with pytest.raises(DryRunOperationException): + execute_dry_run_capable(lambda: "should not run", dry_run=True) + + +@pytest.mark.parametrize( + "invalid_spec", + [ + "INVALID_SPEC", + "AES_256", # Symmetric, not key pair + "", + "foo", + ], +) +@pytest.mark.parametrize("dry_run", [True, False]) +def test_generate_data_key_pair_invalid_spec_raises_unsupported_exception( + provider, invalid_spec, dry_run +): + # Arrange + context = RequestContext() + context.account_id = "000000000000" + context.region = "us-east-1" + + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # Act & Assert + with pytest.raises(UnsupportedOperationException): + provider.generate_data_key_pair( + context=context, + key_id=key_id, + key_pair_spec=invalid_spec, + dry_run=dry_run, + ) + + +@pytest.mark.parametrize( + "invalid_spec", + [ + "RSA_1024", + "ECC_FAKE", # Symmetric, not key pair + "HMAC_222", + ], +) +@pytest.mark.parametrize("dry_run", [True, False]) +def test_generate_data_key_pair_invalid_spec_raises_validation_exception( + provider, invalid_spec, dry_run +): + # Arrange + context = RequestContext() + context.account_id = "000000000000" + context.region = "us-east-1" + + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # Act & Assert + with pytest.raises(ValidationException): + provider.generate_data_key_pair( + context=context, + key_id=key_id, + key_pair_spec=invalid_spec, + dry_run=dry_run, + ) + + +def test_generate_data_key_pair_real_key(provider): + # Arrange + account_id = "000000000000" + region_name = "us-east-1" + context = RequestContext() + context.account_id = account_id + context.region = region_name + + # Note: we're using `provider.create_key` to set up the test, which introduces a hidden dependency. + # If `create_key` fails or changes its behavior, this test might fail incorrectly even if the logic + # under test (`generate_data_key_pair`) is still correct. Ideally, we would decouple the store + # through dependency injection (e.g., by abstracting the KMS store), so that + # we could stub it or inject a pre-populated instance directly in the test setup. + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # # Act + response = provider.generate_data_key_pair( + context=context, + key_id=key_id, + key_pair_spec="RSA_2048", + dry_run=False, + ) + + # # Assert + assert response["KeyId"] == key["KeyMetadata"]["Arn"] + assert response["KeyPairSpec"] == "RSA_2048" + + +def test_generate_data_key_pair_dry_run(provider): + # Arrange + account_id = "000000000000" + region_name = "us-east-1" + context = RequestContext() + context.account_id = account_id + context.region = region_name + + # Note: we're using `provider.create_key` to set up the test, which introduces a hidden dependency. + # If `create_key` fails or changes its behavior, this test might fail incorrectly even if the logic + # under test (`generate_data_key_pair`) is still correct. Ideally, we would decouple the store + # through dependency injection (e.g., by abstracting the KMS store), so that + # we could stub it or inject a pre-populated instance directly in the test setup. + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # Act & Assert + with pytest.raises(DryRunOperationException): + provider.generate_data_key_pair( + context=context, + key_id=key_id, + key_pair_spec="RSA_2048", + dry_run=True, + ) + + +def test_generate_data_key_pair_without_plaintext(provider): + # Arrange + account_id = "000000000000" + region_name = "us-east-1" + context = RequestContext() + context.account_id = account_id + context.region = region_name + + # Note: we're using `provider.create_key` to set up the test, which introduces a hidden dependency. + # If `create_key` fails or changes its behavior, this test might fail incorrectly even if the logic + # under test (`generate_data_key_pair_without_plaintext`) is still correct. Ideally, we would decouple + # the store through dependency injection to isolate test concerns. + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # Act + response = provider.generate_data_key_pair_without_plaintext( + context=context, + key_id=key_id, + key_pair_spec="RSA_2048", + dry_run=False, + ) + + # Assert + assert response["KeyId"] == key["KeyMetadata"]["Arn"] + assert response["KeyPairSpec"] == "RSA_2048" + assert "PrivateKeyPlaintext" not in response # Confirm plaintext was removed + + +def test_generate_data_key_pair_without_plaintext_dry_run(provider): + # Arrange + account_id = "000000000000" + region_name = "us-east-1" + context = RequestContext() + context.account_id = account_id + context.region = region_name + + key_request = CreateKeyRequest(Description="Test key") + key = provider.create_key(context, key_request) + key_id = key["KeyMetadata"]["KeyId"] + + # Act & Assert + with pytest.raises(DryRunOperationException): + provider.generate_data_key_pair_without_plaintext( + context=context, + key_id=key_id, + key_pair_spec="RSA_2048", + dry_run=True, + ) From 102659bf83412447e99aec57492567f1068eb0a3 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:23:37 +0200 Subject: [PATCH 071/108] Manually update ASF APIs and all dependencies (#12548) --- .../localstack/aws/api/ec2/__init__.py | 14 + .../localstack/aws/api/events/__init__.py | 5 + .../aws/api/resource_groups/__init__.py | 13 +- .../localstack/services/events/provider.py | 4 + .../localstack/services/events/v1/provider.py | 3 + pyproject.toml | 6 +- requirements-base-runtime.txt | 8 +- requirements-dev.txt | 16 +- requirements-runtime.txt | 12 +- requirements-test.txt | 14 +- requirements-typehint.txt | 332 ++++++++++++------ 11 files changed, 281 insertions(+), 146 deletions(-) diff --git a/localstack-core/localstack/aws/api/ec2/__init__.py b/localstack-core/localstack/aws/api/ec2/__init__.py index f15d77efc00ad..51bba5a545c69 100644 --- a/localstack-core/localstack/aws/api/ec2/__init__.py +++ b/localstack-core/localstack/aws/api/ec2/__init__.py @@ -3171,6 +3171,7 @@ class ServiceConnectivityType(StrEnum): class ServiceManaged(StrEnum): alb = "alb" nlb = "nlb" + rnat = "rnat" class ServiceState(StrEnum): @@ -6104,6 +6105,14 @@ class ClientLoginBannerResponseOptions(TypedDict, total=False): BannerText: Optional[String] +class ClientRouteEnforcementOptions(TypedDict, total=False): + Enforced: Optional[Boolean] + + +class ClientRouteEnforcementResponseOptions(TypedDict, total=False): + Enforced: Optional[Boolean] + + class FederatedAuthentication(TypedDict, total=False): SamlProviderArn: Optional[String] SelfServiceSamlProviderArn: Optional[String] @@ -6202,6 +6211,7 @@ class ClientVpnEndpoint(TypedDict, total=False): ClientConnectOptions: Optional[ClientConnectResponseOptions] SessionTimeoutHours: Optional[Integer] ClientLoginBannerOptions: Optional[ClientLoginBannerResponseOptions] + ClientRouteEnforcementOptions: Optional[ClientRouteEnforcementResponseOptions] DisconnectOnSessionTimeout: Optional[Boolean] @@ -6551,6 +6561,7 @@ class CreateClientVpnEndpointRequest(ServiceRequest): ClientConnectOptions: Optional[ClientConnectOptions] SessionTimeoutHours: Optional[Integer] ClientLoginBannerOptions: Optional[ClientLoginBannerOptions] + ClientRouteEnforcementOptions: Optional[ClientRouteEnforcementOptions] DisconnectOnSessionTimeout: Optional[Boolean] @@ -18054,6 +18065,7 @@ class ModifyClientVpnEndpointRequest(ServiceRequest): ClientConnectOptions: Optional[ClientConnectOptions] SessionTimeoutHours: Optional[Integer] ClientLoginBannerOptions: Optional[ClientLoginBannerOptions] + ClientRouteEnforcementOptions: Optional[ClientRouteEnforcementOptions] DisconnectOnSessionTimeout: Optional[Boolean] @@ -20993,6 +21005,7 @@ def create_client_vpn_endpoint( client_connect_options: ClientConnectOptions = None, session_timeout_hours: Integer = None, client_login_banner_options: ClientLoginBannerOptions = None, + client_route_enforcement_options: ClientRouteEnforcementOptions = None, disconnect_on_session_timeout: Boolean = None, **kwargs, ) -> CreateClientVpnEndpointResult: @@ -26572,6 +26585,7 @@ def modify_client_vpn_endpoint( client_connect_options: ClientConnectOptions = None, session_timeout_hours: Integer = None, client_login_banner_options: ClientLoginBannerOptions = None, + client_route_enforcement_options: ClientRouteEnforcementOptions = None, disconnect_on_session_timeout: Boolean = None, **kwargs, ) -> ModifyClientVpnEndpointResult: diff --git a/localstack-core/localstack/aws/api/events/__init__.py b/localstack-core/localstack/aws/api/events/__init__.py index fa88310b47693..680a3e1ef3328 100644 --- a/localstack-core/localstack/aws/api/events/__init__.py +++ b/localstack-core/localstack/aws/api/events/__init__.py @@ -548,6 +548,7 @@ class CreateConnectionRequest(ServiceRequest): AuthorizationType: ConnectionAuthorizationType AuthParameters: CreateConnectionAuthRequestParameters InvocationConnectivityParameters: Optional[ConnectivityResourceParameters] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] class CreateConnectionResponse(TypedDict, total=False): @@ -757,6 +758,7 @@ class DescribeConnectionResponse(TypedDict, total=False): StateReason: Optional[ConnectionStateReason] AuthorizationType: Optional[ConnectionAuthorizationType] SecretArn: Optional[SecretsManagerSecretArn] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] AuthParameters: Optional[ConnectionAuthResponseParameters] CreationTime: Optional[Timestamp] LastModifiedTime: Optional[Timestamp] @@ -1504,6 +1506,7 @@ class UpdateConnectionRequest(ServiceRequest): AuthorizationType: Optional[ConnectionAuthorizationType] AuthParameters: Optional[UpdateConnectionAuthRequestParameters] InvocationConnectivityParameters: Optional[ConnectivityResourceParameters] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] class UpdateConnectionResponse(TypedDict, total=False): @@ -1603,6 +1606,7 @@ def create_connection( auth_parameters: CreateConnectionAuthRequestParameters, description: ConnectionDescription = None, invocation_connectivity_parameters: ConnectivityResourceParameters = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> CreateConnectionResponse: raise NotImplementedError @@ -2072,6 +2076,7 @@ def update_connection( authorization_type: ConnectionAuthorizationType = None, auth_parameters: UpdateConnectionAuthRequestParameters = None, invocation_connectivity_parameters: ConnectivityResourceParameters = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> UpdateConnectionResponse: raise NotImplementedError diff --git a/localstack-core/localstack/aws/api/resource_groups/__init__.py b/localstack-core/localstack/aws/api/resource_groups/__init__.py index 4e9f669dcefff..42e0f7f5a3eb1 100644 --- a/localstack-core/localstack/aws/api/resource_groups/__init__.py +++ b/localstack-core/localstack/aws/api/resource_groups/__init__.py @@ -287,6 +287,7 @@ class GetTagSyncTaskOutput(TypedDict, total=False): TaskArn: Optional[TagSyncTaskArn] TagKey: Optional[TagKey] TagValue: Optional[TagValue] + ResourceQuery: Optional[ResourceQuery] RoleArn: Optional[RoleArn] Status: Optional[TagSyncTaskStatus] ErrorMessage: Optional[ErrorMessage] @@ -463,6 +464,7 @@ class TagSyncTaskItem(TypedDict, total=False): TaskArn: Optional[TagSyncTaskArn] TagKey: Optional[TagKey] TagValue: Optional[TagValue] + ResourceQuery: Optional[ResourceQuery] RoleArn: Optional[RoleArn] Status: Optional[TagSyncTaskStatus] ErrorMessage: Optional[ErrorMessage] @@ -500,8 +502,9 @@ class SearchResourcesOutput(TypedDict, total=False): class StartTagSyncTaskInput(ServiceRequest): Group: GroupStringV2 - TagKey: TagKey - TagValue: TagValue + TagKey: Optional[TagKey] + TagValue: Optional[TagValue] + ResourceQuery: Optional[ResourceQuery] RoleArn: RoleArn @@ -511,6 +514,7 @@ class StartTagSyncTaskOutput(TypedDict, total=False): TaskArn: Optional[TagSyncTaskArn] TagKey: Optional[TagKey] TagValue: Optional[TagValue] + ResourceQuery: Optional[ResourceQuery] RoleArn: Optional[RoleArn] @@ -738,9 +742,10 @@ def start_tag_sync_task( self, context: RequestContext, group: GroupStringV2, - tag_key: TagKey, - tag_value: TagValue, role_arn: RoleArn, + tag_key: TagKey = None, + tag_value: TagValue = None, + resource_query: ResourceQuery = None, **kwargs, ) -> StartTagSyncTaskOutput: raise NotImplementedError diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index 1a5b4ab485689..a26f5c63126ed 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -396,8 +396,10 @@ def create_connection( auth_parameters: CreateConnectionAuthRequestParameters, description: ConnectionDescription = None, invocation_connectivity_parameters: ConnectivityResourceParameters = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> CreateConnectionResponse: + # TODO add support for kms_key_identifier region = context.region account_id = context.account_id store = self.get_store(region, account_id) @@ -490,8 +492,10 @@ def update_connection( authorization_type: ConnectionAuthorizationType = None, auth_parameters: UpdateConnectionAuthRequestParameters = None, invocation_connectivity_parameters: ConnectivityResourceParameters = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> UpdateConnectionResponse: + # TODO add support for kms_key_identifier region = context.region account_id = context.account_id store = self.get_store(region, account_id) diff --git a/localstack-core/localstack/services/events/v1/provider.py b/localstack-core/localstack/services/events/v1/provider.py index bbcd4e0ac33eb..9e3da8e447f6a 100644 --- a/localstack-core/localstack/services/events/v1/provider.py +++ b/localstack-core/localstack/services/events/v1/provider.py @@ -25,6 +25,7 @@ EventBusNameOrArn, EventPattern, EventsApi, + KmsKeyIdentifier, PutRuleResponse, PutTargetsResponse, RoleArn, @@ -296,8 +297,10 @@ def create_connection( auth_parameters: CreateConnectionAuthRequestParameters, description: ConnectionDescription = None, invocation_connectivity_parameters: ConnectivityResourceParameters = None, + kms_key_identifier: KmsKeyIdentifier = None, **kwargs, ) -> CreateConnectionResponse: + # TODO add support for kms_key_identifier errors = [] if not CONNECTION_NAME_PATTERN.match(name): diff --git a/pyproject.toml b/pyproject.toml index 7b2a573c685e5..3dbdac8b03d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.37.28", + "boto3==1.38.0", # pinned / updated by ASF update action - "botocore==1.37.28", + "botocore==1.38.0", "awscrt>=0.13.14", "cbor2>=5.5.0", "dnspython>=1.16.0", @@ -78,7 +78,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli>=1.32.117", + "awscli>=1.37.0", "airspeed-ext>=0.6.3", "amazon_kclpy>=3.0.0", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index de16eb6f3fbc4..b3bb668e42b89 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -11,9 +11,9 @@ attrs==25.3.0 # referencing awscrt==0.26.1 # via localstack-core (pyproject.toml) -boto3==1.37.28 +boto3==1.38.0 # via localstack-core (pyproject.toml) -botocore==1.37.28 +botocore==1.38.0 # via # boto3 # localstack-core (pyproject.toml) @@ -102,7 +102,7 @@ markupsafe==3.0.2 # via werkzeug mdurl==0.1.2 # via markdown-it-py -more-itertools==10.6.0 +more-itertools==10.7.0 # via openapi-core openapi-core==0.19.4 # via localstack-core (pyproject.toml) @@ -170,7 +170,7 @@ rpds-py==0.24.0 # via # jsonschema # referencing -s3transfer==0.11.5 +s3transfer==0.12.0 # via boto3 semver==3.0.4 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index ebaa57577d5f8..3d537a29c2646 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.190.0 +aws-cdk-lib==2.191.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -41,17 +41,17 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.28 +awscli==1.39.0 # via localstack-core awscrt==0.26.1 # via localstack-core -boto3==1.37.28 +boto3==1.38.0 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.28 +botocore==1.38.0 # via # aws-xray-sdk # awscli @@ -140,7 +140,7 @@ docker==7.1.0 # moto-ext docopt==0.6.2 # via coveralls -docutils==0.16 +docutils==0.19 # via awscli events==0.5 # via opensearch-py @@ -248,7 +248,7 @@ markupsafe==3.0.2 # werkzeug mdurl==0.1.2 # via markdown-it-py -more-itertools==10.6.0 +more-itertools==10.7.0 # via openapi-core moto-ext==5.1.3.post1 # via localstack-core @@ -258,7 +258,7 @@ multipart==1.2.1 # via moto-ext mypy==1.15.0 # via localstack-core (pyproject.toml) -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy networkx==3.4.2 # via @@ -431,7 +431,7 @@ rstr==3.2.2 # via localstack-core (pyproject.toml) ruff==0.11.6 # via localstack-core (pyproject.toml) -s3transfer==0.11.5 +s3transfer==0.12.0 # via # awscli # boto3 diff --git a/requirements-runtime.txt b/requirements-runtime.txt index f565f23b99873..5d01d86822399 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -29,17 +29,17 @@ aws-sam-translator==1.97.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.28 +awscli==1.39.0 # via localstack-core (pyproject.toml) awscrt==0.26.1 # via localstack-core -boto3==1.37.28 +boto3==1.38.0 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.28 +botocore==1.38.0 # via # aws-xray-sdk # awscli @@ -104,7 +104,7 @@ docker==7.1.0 # via # localstack-core # moto-ext -docutils==0.16 +docutils==0.19 # via awscli events==0.5 # via opensearch-py @@ -186,7 +186,7 @@ markupsafe==3.0.2 # werkzeug mdurl==0.1.2 # via markdown-it-py -more-itertools==10.6.0 +more-itertools==10.7.0 # via openapi-core moto-ext==5.1.3.post1 # via localstack-core (pyproject.toml) @@ -312,7 +312,7 @@ rpds-py==0.24.0 # referencing rsa==4.7.2 # via awscli -s3transfer==0.11.5 +s3transfer==0.12.0 # via # awscli # boto3 diff --git a/requirements-test.txt b/requirements-test.txt index e1d422607182f..f7cf447cce7ab 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.190.0 +aws-cdk-lib==2.191.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.97.0 # via @@ -41,17 +41,17 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.28 +awscli==1.39.0 # via localstack-core awscrt==0.26.1 # via localstack-core -boto3==1.37.28 +boto3==1.38.0 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.37.28 +botocore==1.38.0 # via # aws-xray-sdk # awscli @@ -128,7 +128,7 @@ docker==7.1.0 # via # localstack-core # moto-ext -docutils==0.16 +docutils==0.19 # via awscli events==0.5 # via opensearch-py @@ -232,7 +232,7 @@ markupsafe==3.0.2 # werkzeug mdurl==0.1.2 # via markdown-it-py -more-itertools==10.6.0 +more-itertools==10.7.0 # via openapi-core moto-ext==5.1.3.post1 # via localstack-core @@ -389,7 +389,7 @@ rpds-py==0.24.0 # referencing rsa==4.7.2 # via awscli -s3transfer==0.11.5 +s3transfer==0.12.0 # via # awscli # boto3 diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 1ceea72941f1f..1070b7c41cc93 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.190.0 +aws-cdk-lib==2.191.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -41,19 +41,19 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.38.28 +awscli==1.39.0 # via localstack-core awscrt==0.26.1 # via localstack-core -boto3==1.37.28 +boto3==1.38.0 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.37.38 +boto3-stubs==1.38.0 # via localstack-core (pyproject.toml) -botocore==1.37.28 +botocore==1.38.0 # via # aws-xray-sdk # awscli @@ -61,7 +61,7 @@ botocore==1.37.28 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.37.38 +botocore-stubs==1.38.0 # via boto3-stubs build==1.2.2.post1 # via @@ -144,7 +144,7 @@ docker==7.1.0 # moto-ext docopt==0.6.2 # via coveralls -docutils==0.16 +docutils==0.19 # via awscli events==0.5 # via opensearch-py @@ -252,7 +252,7 @@ markupsafe==3.0.2 # werkzeug mdurl==0.1.2 # via markdown-it-py -more-itertools==10.6.0 +more-itertools==10.7.0 # via openapi-core moto-ext==5.1.3.post1 # via localstack-core @@ -262,213 +262,213 @@ multipart==1.2.1 # via moto-ext mypy==1.15.0 # via localstack-core -mypy-boto3-acm==1.37.0 +mypy-boto3-acm==1.38.0 # via boto3-stubs -mypy-boto3-acm-pca==1.37.12 +mypy-boto3-acm-pca==1.38.0 # via boto3-stubs -mypy-boto3-amplify==1.37.17 +mypy-boto3-amplify==1.38.0 # via boto3-stubs -mypy-boto3-apigateway==1.37.23 +mypy-boto3-apigateway==1.38.0 # via boto3-stubs -mypy-boto3-apigatewayv2==1.37.23 +mypy-boto3-apigatewayv2==1.38.0 # via boto3-stubs -mypy-boto3-appconfig==1.37.0 +mypy-boto3-appconfig==1.38.0 # via boto3-stubs -mypy-boto3-appconfigdata==1.37.0 +mypy-boto3-appconfigdata==1.38.0 # via boto3-stubs -mypy-boto3-application-autoscaling==1.37.32 +mypy-boto3-application-autoscaling==1.38.0 # via boto3-stubs -mypy-boto3-appsync==1.37.15 +mypy-boto3-appsync==1.38.0 # via boto3-stubs -mypy-boto3-athena==1.37.0 +mypy-boto3-athena==1.38.0 # via boto3-stubs -mypy-boto3-autoscaling==1.37.36 +mypy-boto3-autoscaling==1.38.0 # via boto3-stubs -mypy-boto3-backup==1.37.0 +mypy-boto3-backup==1.38.0 # via boto3-stubs -mypy-boto3-batch==1.37.22 +mypy-boto3-batch==1.38.0 # via boto3-stubs -mypy-boto3-ce==1.37.30 +mypy-boto3-ce==1.38.0 # via boto3-stubs -mypy-boto3-cloudcontrol==1.37.0 +mypy-boto3-cloudcontrol==1.38.0 # via boto3-stubs -mypy-boto3-cloudformation==1.37.22 +mypy-boto3-cloudformation==1.38.0 # via boto3-stubs -mypy-boto3-cloudfront==1.37.9 +mypy-boto3-cloudfront==1.38.0 # via boto3-stubs -mypy-boto3-cloudtrail==1.37.8 +mypy-boto3-cloudtrail==1.38.0 # via boto3-stubs -mypy-boto3-cloudwatch==1.37.0 +mypy-boto3-cloudwatch==1.38.0 # via boto3-stubs -mypy-boto3-codebuild==1.37.29 +mypy-boto3-codebuild==1.38.0 # via boto3-stubs -mypy-boto3-codecommit==1.37.0 +mypy-boto3-codecommit==1.38.0 # via boto3-stubs -mypy-boto3-codeconnections==1.37.0 +mypy-boto3-codeconnections==1.38.0 # via boto3-stubs -mypy-boto3-codedeploy==1.37.0 +mypy-boto3-codedeploy==1.38.0 # via boto3-stubs -mypy-boto3-codepipeline==1.37.0 +mypy-boto3-codepipeline==1.38.0 # via boto3-stubs -mypy-boto3-codestar-connections==1.37.0 +mypy-boto3-codestar-connections==1.38.0 # via boto3-stubs -mypy-boto3-cognito-identity==1.37.13 +mypy-boto3-cognito-identity==1.38.0 # via boto3-stubs -mypy-boto3-cognito-idp==1.37.13.post1 +mypy-boto3-cognito-idp==1.38.0 # via boto3-stubs -mypy-boto3-dms==1.37.4 +mypy-boto3-dms==1.38.0 # via boto3-stubs -mypy-boto3-docdb==1.37.0 +mypy-boto3-docdb==1.38.0 # via boto3-stubs -mypy-boto3-dynamodb==1.37.33 +mypy-boto3-dynamodb==1.38.0 # via boto3-stubs -mypy-boto3-dynamodbstreams==1.37.0 +mypy-boto3-dynamodbstreams==1.38.0 # via boto3-stubs -mypy-boto3-ec2==1.37.28 +mypy-boto3-ec2==1.38.0 # via boto3-stubs -mypy-boto3-ecr==1.37.26 +mypy-boto3-ecr==1.38.0 # via boto3-stubs -mypy-boto3-ecs==1.37.36 +mypy-boto3-ecs==1.38.0 # via boto3-stubs -mypy-boto3-efs==1.37.0 +mypy-boto3-efs==1.38.0 # via boto3-stubs -mypy-boto3-eks==1.37.35 +mypy-boto3-eks==1.38.0 # via boto3-stubs -mypy-boto3-elasticache==1.37.32 +mypy-boto3-elasticache==1.38.0 # via boto3-stubs -mypy-boto3-elasticbeanstalk==1.37.0 +mypy-boto3-elasticbeanstalk==1.38.0 # via boto3-stubs -mypy-boto3-elbv2==1.37.9 +mypy-boto3-elbv2==1.38.0 # via boto3-stubs -mypy-boto3-emr==1.37.3 +mypy-boto3-emr==1.38.0 # via boto3-stubs -mypy-boto3-emr-serverless==1.37.0 +mypy-boto3-emr-serverless==1.38.0 # via boto3-stubs -mypy-boto3-es==1.37.0 +mypy-boto3-es==1.38.0 # via boto3-stubs -mypy-boto3-events==1.37.35 +mypy-boto3-events==1.38.0 # via boto3-stubs -mypy-boto3-firehose==1.37.38 +mypy-boto3-firehose==1.38.0 # via boto3-stubs -mypy-boto3-fis==1.37.0 +mypy-boto3-fis==1.38.0 # via boto3-stubs -mypy-boto3-glacier==1.37.0 +mypy-boto3-glacier==1.38.0 # via boto3-stubs -mypy-boto3-glue==1.37.31 +mypy-boto3-glue==1.38.0 # via boto3-stubs -mypy-boto3-iam==1.37.22 +mypy-boto3-iam==1.38.0 # via boto3-stubs -mypy-boto3-identitystore==1.37.0 +mypy-boto3-identitystore==1.38.0 # via boto3-stubs -mypy-boto3-iot==1.37.1 +mypy-boto3-iot==1.38.0 # via boto3-stubs -mypy-boto3-iot-data==1.37.0 +mypy-boto3-iot-data==1.38.0 # via boto3-stubs -mypy-boto3-iotanalytics==1.37.0 +mypy-boto3-iotanalytics==1.38.0 # via boto3-stubs -mypy-boto3-iotwireless==1.37.19 +mypy-boto3-iotwireless==1.38.0 # via boto3-stubs -mypy-boto3-kafka==1.37.0 +mypy-boto3-kafka==1.38.0 # via boto3-stubs -mypy-boto3-kinesis==1.37.0 +mypy-boto3-kinesis==1.38.0 # via boto3-stubs -mypy-boto3-kinesisanalytics==1.37.0 +mypy-boto3-kinesisanalytics==1.38.0 # via boto3-stubs -mypy-boto3-kinesisanalyticsv2==1.37.0 +mypy-boto3-kinesisanalyticsv2==1.38.0 # via boto3-stubs -mypy-boto3-kms==1.37.0 +mypy-boto3-kms==1.38.0 # via boto3-stubs -mypy-boto3-lakeformation==1.37.13 +mypy-boto3-lakeformation==1.38.0 # via boto3-stubs -mypy-boto3-lambda==1.37.16 +mypy-boto3-lambda==1.38.0 # via boto3-stubs -mypy-boto3-logs==1.37.12 +mypy-boto3-logs==1.38.0 # via boto3-stubs -mypy-boto3-managedblockchain==1.37.0 +mypy-boto3-managedblockchain==1.38.0 # via boto3-stubs -mypy-boto3-mediaconvert==1.37.21 +mypy-boto3-mediaconvert==1.38.0 # via boto3-stubs -mypy-boto3-mediastore==1.37.0 +mypy-boto3-mediastore==1.38.0 # via boto3-stubs -mypy-boto3-mq==1.37.0 +mypy-boto3-mq==1.38.0 # via boto3-stubs -mypy-boto3-mwaa==1.37.0 +mypy-boto3-mwaa==1.38.0 # via boto3-stubs -mypy-boto3-neptune==1.37.0 +mypy-boto3-neptune==1.38.0 # via boto3-stubs -mypy-boto3-opensearch==1.37.27 +mypy-boto3-opensearch==1.38.0 # via boto3-stubs -mypy-boto3-organizations==1.37.0 +mypy-boto3-organizations==1.38.0 # via boto3-stubs -mypy-boto3-pi==1.37.0 +mypy-boto3-pi==1.38.0 # via boto3-stubs -mypy-boto3-pinpoint==1.37.0 +mypy-boto3-pinpoint==1.38.0 # via boto3-stubs -mypy-boto3-pipes==1.37.0 +mypy-boto3-pipes==1.38.0 # via boto3-stubs -mypy-boto3-qldb==1.37.0 +mypy-boto3-qldb==1.38.0 # via boto3-stubs -mypy-boto3-qldb-session==1.37.0 +mypy-boto3-qldb-session==1.38.0 # via boto3-stubs -mypy-boto3-rds==1.37.21 +mypy-boto3-rds==1.38.0 # via boto3-stubs -mypy-boto3-rds-data==1.37.0 +mypy-boto3-rds-data==1.38.0 # via boto3-stubs -mypy-boto3-redshift==1.37.0 +mypy-boto3-redshift==1.38.0 # via boto3-stubs -mypy-boto3-redshift-data==1.37.8 +mypy-boto3-redshift-data==1.38.0 # via boto3-stubs -mypy-boto3-resource-groups==1.37.35 +mypy-boto3-resource-groups==1.38.0 # via boto3-stubs -mypy-boto3-resourcegroupstaggingapi==1.37.0 +mypy-boto3-resourcegroupstaggingapi==1.38.0 # via boto3-stubs -mypy-boto3-route53==1.37.27 +mypy-boto3-route53==1.38.0 # via boto3-stubs -mypy-boto3-route53resolver==1.37.0 +mypy-boto3-route53resolver==1.38.0 # via boto3-stubs -mypy-boto3-s3==1.37.24 +mypy-boto3-s3==1.38.0 # via boto3-stubs -mypy-boto3-s3control==1.37.28 +mypy-boto3-s3control==1.38.0 # via boto3-stubs -mypy-boto3-sagemaker==1.37.37 +mypy-boto3-sagemaker==1.38.0 # via boto3-stubs -mypy-boto3-sagemaker-runtime==1.37.0 +mypy-boto3-sagemaker-runtime==1.38.0 # via boto3-stubs -mypy-boto3-secretsmanager==1.37.0 +mypy-boto3-secretsmanager==1.38.0 # via boto3-stubs -mypy-boto3-serverlessrepo==1.37.0 +mypy-boto3-serverlessrepo==1.38.0 # via boto3-stubs -mypy-boto3-servicediscovery==1.37.0 +mypy-boto3-servicediscovery==1.38.0 # via boto3-stubs -mypy-boto3-ses==1.37.0 +mypy-boto3-ses==1.38.0 # via boto3-stubs -mypy-boto3-sesv2==1.37.27 +mypy-boto3-sesv2==1.38.0 # via boto3-stubs -mypy-boto3-sns==1.37.0 +mypy-boto3-sns==1.38.0 # via boto3-stubs -mypy-boto3-sqs==1.37.0 +mypy-boto3-sqs==1.38.0 # via boto3-stubs -mypy-boto3-ssm==1.37.19 +mypy-boto3-ssm==1.38.0 # via boto3-stubs -mypy-boto3-sso-admin==1.37.0 +mypy-boto3-sso-admin==1.38.0 # via boto3-stubs -mypy-boto3-stepfunctions==1.37.0 +mypy-boto3-stepfunctions==1.38.0 # via boto3-stubs -mypy-boto3-sts==1.37.0 +mypy-boto3-sts==1.38.0 # via boto3-stubs -mypy-boto3-timestream-query==1.37.0 +mypy-boto3-timestream-query==1.38.0 # via boto3-stubs -mypy-boto3-timestream-write==1.37.0 +mypy-boto3-timestream-write==1.38.0 # via boto3-stubs -mypy-boto3-transcribe==1.37.27 +mypy-boto3-transcribe==1.38.0 # via boto3-stubs -mypy-boto3-verifiedpermissions==1.37.33 +mypy-boto3-verifiedpermissions==1.38.0 # via boto3-stubs -mypy-boto3-wafv2==1.37.21 +mypy-boto3-wafv2==1.38.0 # via boto3-stubs -mypy-boto3-xray==1.37.0 +mypy-boto3-xray==1.38.0 # via boto3-stubs -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy networkx==3.4.2 # via @@ -641,7 +641,7 @@ rstr==3.2.2 # via localstack-core ruff==0.11.6 # via localstack-core -s3transfer==0.11.5 +s3transfer==0.12.0 # via # awscli # boto3 @@ -673,16 +673,120 @@ typeguard==2.13.3 # jsii types-awscrt==0.26.1 # via botocore-stubs -types-s3transfer==0.11.5 +types-s3transfer==0.12.0 # via boto3-stubs typing-extensions==4.13.2 # via # anyio # aws-sam-translator + # boto3-stubs # cfn-lint # jsii # localstack-twisted # mypy + # mypy-boto3-acm + # mypy-boto3-acm-pca + # mypy-boto3-amplify + # mypy-boto3-apigateway + # mypy-boto3-apigatewayv2 + # mypy-boto3-appconfig + # mypy-boto3-appconfigdata + # mypy-boto3-application-autoscaling + # mypy-boto3-appsync + # mypy-boto3-athena + # mypy-boto3-autoscaling + # mypy-boto3-backup + # mypy-boto3-batch + # mypy-boto3-ce + # mypy-boto3-cloudcontrol + # mypy-boto3-cloudformation + # mypy-boto3-cloudfront + # mypy-boto3-cloudtrail + # mypy-boto3-cloudwatch + # mypy-boto3-codebuild + # mypy-boto3-codecommit + # mypy-boto3-codeconnections + # mypy-boto3-codedeploy + # mypy-boto3-codepipeline + # mypy-boto3-codestar-connections + # mypy-boto3-cognito-identity + # mypy-boto3-cognito-idp + # mypy-boto3-dms + # mypy-boto3-docdb + # mypy-boto3-dynamodb + # mypy-boto3-dynamodbstreams + # mypy-boto3-ec2 + # mypy-boto3-ecr + # mypy-boto3-ecs + # mypy-boto3-efs + # mypy-boto3-eks + # mypy-boto3-elasticache + # mypy-boto3-elasticbeanstalk + # mypy-boto3-elbv2 + # mypy-boto3-emr + # mypy-boto3-emr-serverless + # mypy-boto3-es + # mypy-boto3-events + # mypy-boto3-firehose + # mypy-boto3-fis + # mypy-boto3-glacier + # mypy-boto3-glue + # mypy-boto3-iam + # mypy-boto3-identitystore + # mypy-boto3-iot + # mypy-boto3-iot-data + # mypy-boto3-iotanalytics + # mypy-boto3-iotwireless + # mypy-boto3-kafka + # mypy-boto3-kinesis + # mypy-boto3-kinesisanalytics + # mypy-boto3-kinesisanalyticsv2 + # mypy-boto3-kms + # mypy-boto3-lakeformation + # mypy-boto3-lambda + # mypy-boto3-logs + # mypy-boto3-managedblockchain + # mypy-boto3-mediaconvert + # mypy-boto3-mediastore + # mypy-boto3-mq + # mypy-boto3-mwaa + # mypy-boto3-neptune + # mypy-boto3-opensearch + # mypy-boto3-organizations + # mypy-boto3-pi + # mypy-boto3-pinpoint + # mypy-boto3-pipes + # mypy-boto3-qldb + # mypy-boto3-qldb-session + # mypy-boto3-rds + # mypy-boto3-rds-data + # mypy-boto3-redshift + # mypy-boto3-redshift-data + # mypy-boto3-resource-groups + # mypy-boto3-resourcegroupstaggingapi + # mypy-boto3-route53 + # mypy-boto3-route53resolver + # mypy-boto3-s3 + # mypy-boto3-s3control + # mypy-boto3-sagemaker + # mypy-boto3-sagemaker-runtime + # mypy-boto3-secretsmanager + # mypy-boto3-serverlessrepo + # mypy-boto3-servicediscovery + # mypy-boto3-ses + # mypy-boto3-sesv2 + # mypy-boto3-sns + # mypy-boto3-sqs + # mypy-boto3-ssm + # mypy-boto3-sso-admin + # mypy-boto3-stepfunctions + # mypy-boto3-sts + # mypy-boto3-timestream-query + # mypy-boto3-timestream-write + # mypy-boto3-transcribe + # mypy-boto3-verifiedpermissions + # mypy-boto3-wafv2 + # mypy-boto3-xray # pydantic # pydantic-core # pyopenssl From bebff5e1dcb1a4694caf82447fa25d2c45c9972d Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:17:08 +0200 Subject: [PATCH 072/108] Lambda: fix transient connection errors on first container invoke with retry logic (#12522) --- .../lambda_/invocation/executor_endpoint.py | 70 ++++++++++++++++++- .../stepfunctions/asl/utils/boto_client.py | 5 +- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/invocation/executor_endpoint.py b/localstack-core/localstack/services/lambda_/invocation/executor_endpoint.py index 757dab5d08324..eea6e0c77ebaa 100644 --- a/localstack-core/localstack/services/lambda_/invocation/executor_endpoint.py +++ b/localstack-core/localstack/services/lambda_/invocation/executor_endpoint.py @@ -1,8 +1,9 @@ import abc import logging +import time from concurrent.futures import CancelledError, Future from http import HTTPStatus -from typing import Dict, Optional +from typing import Any, Dict, Optional import requests from werkzeug import Request @@ -10,6 +11,7 @@ from localstack.http import Response, route from localstack.services.edge import ROUTER from localstack.services.lambda_.invocation.lambda_models import InvocationResult +from localstack.utils.backoff import ExponentialBackoff from localstack.utils.lambda_debug_mode.lambda_debug_mode import ( DEFAULT_LAMBDA_DEBUG_MODE_TIMEOUT_SECONDS, is_lambda_debug_mode, @@ -192,7 +194,9 @@ def invoke(self, payload: Dict[str, str]) -> InvocationResult: invocation_url = f"http://{self.container_address}:{self.container_port}/invoke" # disable proxies for internal requests proxies = {"http": "", "https": ""} - response = requests.post(url=invocation_url, json=payload, proxies=proxies) + response = self._perform_invoke( + invocation_url=invocation_url, proxies=proxies, payload=payload + ) if not response.ok: raise InvokeSendError( f"Error while sending invocation {payload} to {invocation_url}. Error Code: {response.status_code}" @@ -214,3 +218,65 @@ def invoke(self, payload: Dict[str, str]) -> InvocationResult: invoke_timeout_buffer_seconds = 5 timeout_seconds = lambda_max_timeout_seconds + invoke_timeout_buffer_seconds return self.invocation_future.result(timeout=timeout_seconds) + + @staticmethod + def _perform_invoke( + invocation_url: str, + proxies: dict[str, str], + payload: dict[str, Any], + ) -> requests.Response: + """ + Dispatches a Lambda invocation request to the specified container endpoint, with automatic + retries in case of connection errors, using exponential backoff. + + The first attempt is made immediately. If it fails, exponential backoff is applied with + retry intervals starting at 100ms, doubling each time for up to 5 total retries. + + Parameters: + invocation_url (str): The full URL of the container's invocation endpoint. + proxies (dict[str, str]): Proxy settings to be used for the HTTP request. + payload (dict[str, Any]): The JSON payload to send to the container. + + Returns: + Response: The successful HTTP response from the container. + + Raises: + requests.exceptions.ConnectionError: If all retry attempts fail to connect. + """ + backoff = None + last_exception = None + max_retry_on_connection_error = 5 + + for attempt_count in range(max_retry_on_connection_error + 1): # 1 initial + n retries + try: + response = requests.post(url=invocation_url, json=payload, proxies=proxies) + return response + except requests.exceptions.ConnectionError as connection_error: + last_exception = connection_error + + if backoff is None: + LOG.debug( + "Initial connection attempt failed: %s. Starting backoff retries.", + connection_error, + ) + backoff = ExponentialBackoff( + max_retries=max_retry_on_connection_error, + initial_interval=0.1, + multiplier=2.0, + randomization_factor=0.0, + max_interval=1, + max_time_elapsed=-1, + ) + + delay = backoff.next_backoff() + if delay > 0: + LOG.debug( + "Connection error on invoke attempt #%d: %s. Retrying in %.2f seconds", + attempt_count, + connection_error, + delay, + ) + time.sleep(delay) + + LOG.debug("Connection error after all attempts exhausted: %s", last_exception) + raise last_exception diff --git a/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py b/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py index e2f17cba8a54b..c7facf1bb532c 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py +++ b/localstack-core/localstack/services/stepfunctions/asl/utils/boto_client.py @@ -10,10 +10,7 @@ _BOTO_CLIENT_CONFIG = config = Config( parameter_validation=False, - # Temporary workaround—should be reverted once underlying potential Lambda limitation is resolved. - # Increased total boto client retry attempts from 1 to 5 to mitigate transient service issues. - # This helps reduce unnecessary state machine retries on non-service-level errors. - retries={"total_max_attempts": 5}, + retries={"total_max_attempts": 1}, connect_timeout=TimeoutSeconds.DEFAULT_TIMEOUT_SECONDS, read_timeout=TimeoutSeconds.DEFAULT_TIMEOUT_SECONDS, tcp_keepalive=True, From 5aefdf23e333b0f5df8b5c4129255f5dbbdef18f Mon Sep 17 00:00:00 2001 From: Greg Furman <31275503+gregfurman@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:56:38 +0200 Subject: [PATCH 073/108] [ESM] Support discarding events exceeding MaxRecordAgeInSeconds (#12531) --- .../esm_event_processor.py | 2 + .../esm_worker_factory.py | 2 + .../pollers/sqs_poller.py | 20 +- .../pollers/stream_poller.py | 69 +++-- .../localstack/services/lambda_/provider.py | 2 + .../test_lambda_integration_kinesis.py | 224 ++++++++++++++ ...t_lambda_integration_kinesis.snapshot.json | 277 ++++++++++++++++++ ...lambda_integration_kinesis.validation.json | 12 + .../test_lambda_integration_sqs.py | 8 +- 9 files changed, 577 insertions(+), 39 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/esm_event_processor.py b/localstack-core/localstack/services/lambda_/event_source_mapping/esm_event_processor.py index 4712f5a4fd3f9..b2e85a04ea26c 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/esm_event_processor.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/esm_event_processor.py @@ -159,6 +159,8 @@ def generate_event_failure_context(self, abort_condition: str, **kwargs) -> dict if not error_payload: return {} # TODO: Should 'requestContext' and 'responseContext' be defined as models? + # TODO: Allow for generating failure context where there is no responseContext i.e + # if a RecordAgeExceeded condition is triggered. context = { "requestContext": { "requestId": error_payload.get("requestId"), diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/esm_worker_factory.py b/localstack-core/localstack/services/lambda_/event_source_mapping/esm_worker_factory.py index 38fdaafc2b537..0bf30dfb15d79 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/esm_worker_factory.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/esm_worker_factory.py @@ -172,6 +172,7 @@ def get_esm_worker(self) -> EsmWorker: "MaximumBatchingWindowInSeconds" ], MaximumRetryAttempts=self.esm_config["MaximumRetryAttempts"], + MaximumRecordAgeInSeconds=self.esm_config["MaximumRecordAgeInSeconds"], **optional_params, ), ) @@ -203,6 +204,7 @@ def get_esm_worker(self) -> EsmWorker: "MaximumBatchingWindowInSeconds" ], MaximumRetryAttempts=self.esm_config["MaximumRetryAttempts"], + MaximumRecordAgeInSeconds=self.esm_config["MaximumRecordAgeInSeconds"], **optional_params, ), ) diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/sqs_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/sqs_poller.py index fd00119dbb08e..d39805dce9113 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/sqs_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/sqs_poller.py @@ -1,3 +1,4 @@ +import functools import json import logging from collections import defaultdict @@ -7,7 +8,7 @@ from localstack.aws.api.pipes import PipeSourceSqsQueueParameters from localstack.aws.api.sqs import MessageSystemAttributeName -from localstack.config import internal_service_url +from localstack.aws.connect import connect_to from localstack.services.lambda_.event_source_mapping.event_processor import ( EventProcessor, PartialBatchFailureError, @@ -315,16 +316,19 @@ def transform_into_events(messages: list[dict]) -> list[dict]: return events +@functools.cache def get_queue_url(queue_arn: str) -> str: - # TODO: consolidate this method with localstack.services.sqs.models.SqsQueue.url - # * Do we need to support different endpoint strategies? - # * If so, how can we achieve this without having a request context - host_url = internal_service_url() - host = host_url.rstrip("/") parsed_arn = parse_arn(queue_arn) + + queue_name = parsed_arn["resource"] account_id = parsed_arn["account"] - name = parsed_arn["resource"] - return f"{host}/{account_id}/{name}" + region = parsed_arn["region"] + + sqs_client = connect_to(region_name=region).sqs + queue_url = sqs_client.get_queue_url(QueueName=queue_name, QueueOwnerAWSAccountId=account_id)[ + "QueueUrl" + ] + return queue_url def message_attributes_to_lower(message_attrs): diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py index c489fa87a9ed6..5ebb51a2f7709 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py @@ -2,6 +2,7 @@ import logging import threading from abc import abstractmethod +from bisect import bisect_left from collections import defaultdict from datetime import datetime from typing import Iterator @@ -209,16 +210,7 @@ def poll_events_from_shard(self, shard_id: str, shard_iterator: str): def forward_events_to_target(self, shard_id, next_shard_iterator, records): polled_events = self.transform_into_events(records, shard_id) - abort_condition = None - # Check MaximumRecordAgeInSeconds - if maximum_record_age_in_seconds := self.stream_parameters.get("MaximumRecordAgeInSeconds"): - arrival_timestamp_of_last_event = polled_events[-1]["approximateArrivalTimestamp"] - now = get_current_time().timestamp() - record_age_in_seconds = now - arrival_timestamp_of_last_event - if record_age_in_seconds > maximum_record_age_in_seconds: - abort_condition = "RecordAgeExpired" - # TODO: implement format detection behavior (e.g., for JSON body): # https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-filtering.html # Check whether we need poller-specific filter-preprocessing here without modifying the actual event! @@ -243,23 +235,32 @@ def forward_events_to_target(self, shard_id, next_shard_iterator, records): return events = self.add_source_metadata(matching_events_post_filter) LOG.debug("Polled %d events from %s in shard %s", len(events), self.source_arn, shard_id) - # TODO: A retry should probably re-trigger fetching the record from the stream again?! # -> This could be tested by setting a high retry number, using a long pipe execution, and a relatively # short record expiration age at the source. Check what happens if the record expires at the source. # A potential implementation could use checkpointing based on the iterator position (within shard scope) # TODO: handle partial batch failure (see poller.py:parse_batch_item_failures) # TODO: think about how to avoid starvation of other shards if one shard runs into infinite retries attempts = 0 + discarded_events_for_dlq = [] error_payload = {} max_retries = self.stream_parameters.get("MaximumRetryAttempts", -1) + max_record_age = max( + self.stream_parameters.get("MaximumRecordAgeInSeconds", -1), 0 + ) # Disable check if -1 # NOTE: max_retries == 0 means exponential backoff is disabled boff = ExponentialBackoff(max_retries=max_retries) - while ( - not abort_condition - and not self.max_retries_exceeded(attempts) - and not self._is_shutdown.is_set() - ): + while not abort_condition and events and not self._is_shutdown.is_set(): + if self.max_retries_exceeded(attempts): + abort_condition = "RetryAttemptsExhausted" + break + + if max_record_age: + events, expired_events = self.bisect_events_by_record_age(max_record_age, events) + if expired_events: + discarded_events_for_dlq.extend(expired_events) + continue + try: if attempts > 0: # TODO: Should we always backoff (with jitter) before processing since we may not want multiple pollers @@ -269,10 +270,8 @@ def forward_events_to_target(self, shard_id, next_shard_iterator, records): self.processor.process_events_batch(events) boff.reset() - - # Update shard iterator if execution is successful - self.shards[shard_id] = next_shard_iterator - return + # We may need to send on data to a DLQ so break the processing loop and proceed if invocation successful. + break except PartialBatchFailureError as ex: # TODO: add tests for partial batch failure scenarios if ( @@ -327,15 +326,20 @@ def forward_events_to_target(self, shard_id, next_shard_iterator, records): # Retry polling until the record expires at the source attempts += 1 + if discarded_events_for_dlq: + abort_condition = "RecordAgeExceeded" + error_payload = {} + events = discarded_events_for_dlq + # Send failed events to potential DLQ - abort_condition = abort_condition or "RetryAttemptsExhausted" - failure_context = self.processor.generate_event_failure_context( - abort_condition=abort_condition, - error=error_payload, - attempts_count=attempts, - partner_resource_arn=self.partner_resource_arn, - ) - self.send_events_to_dlq(shard_id, events, context=failure_context) + if abort_condition: + failure_context = self.processor.generate_event_failure_context( + abort_condition=abort_condition, + error=error_payload, + attempts_count=attempts, + partner_resource_arn=self.partner_resource_arn, + ) + self.send_events_to_dlq(shard_id, events, context=failure_context) # Update shard iterator if the execution failed but the events are sent to a DLQ self.shards[shard_id] = next_shard_iterator @@ -479,6 +483,17 @@ def bisect_events( return events, [] + def bisect_events_by_record_age( + self, maximum_record_age: int, events: list[dict] + ) -> tuple[list[dict], list[dict]]: + """Splits events into [valid_events], [expired_events] based on record age. + Where: + - Events with age < maximum_record_age are valid. + - Events with age >= maximum_record_age are expired.""" + cutoff_timestamp = get_current_time().timestamp() - maximum_record_age + index = bisect_left(events, cutoff_timestamp, key=self.get_approximate_arrival_time) + return events[index:], events[:index] + def get_failure_s3_object_key(esm_uuid: str, shard_id: str, failure_datetime: datetime) -> str: """ diff --git a/localstack-core/localstack/services/lambda_/provider.py b/localstack-core/localstack/services/lambda_/provider.py index a30b8be7afc59..add4c2f8cdd0b 100644 --- a/localstack-core/localstack/services/lambda_/provider.py +++ b/localstack-core/localstack/services/lambda_/provider.py @@ -1988,6 +1988,8 @@ def create_event_source_mapping_v2( def validate_event_source_mapping(self, context, request): # TODO: test whether stream ARNs are valid sources for Pipes or ESM or whether only DynamoDB table ARNs work + # TODO: Validate MaxRecordAgeInSeconds (i.e cannot subceed 60s but can be -1) and MaxRetryAttempts parameters. + # See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-maximumrecordageinseconds is_create_esm_request = context.operation.name == self.create_event_source_mapping.operation if destination_config := request.get("DestinationConfig"): diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py index e7ce14e770f08..b2a864696bb5f 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py @@ -26,6 +26,7 @@ ) from tests.aws.services.lambda_.functions import FUNCTIONS_PATH, lambda_integration from tests.aws.services.lambda_.test_lambda import ( + TEST_LAMBDA_EVENT_SOURCE_MAPPING_SEND_MESSAGE, TEST_LAMBDA_PYTHON, TEST_LAMBDA_PYTHON_ECHO, ) @@ -35,6 +36,7 @@ TEST_LAMBDA_KINESIS_BATCH_ITEM_FAILURE = ( FUNCTIONS_PATH / "lambda_report_batch_item_failures_kinesis.py" ) +TEST_LAMBDA_ECHO_FAILURE = FUNCTIONS_PATH / "lambda_echofail.py" TEST_LAMBDA_PROVIDED_BOOTSTRAP_EMPTY = FUNCTIONS_PATH / "provided_bootstrap_empty" @@ -1054,6 +1056,228 @@ def _verify_messages_received(): invocation_events = retry(_verify_messages_received, retries=30, sleep=5) snapshot.match("kinesis_events", invocation_events) + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # FIXME: Generate and send a requestContext in StreamPoller for RecordAgeExceeded + # which contains no responseContext object. + "$..Messages..Body.requestContext", + "$..Messages..MessageId", # Skip while no requestContext generated in StreamPoller due to transformation issues + ] + ) + @pytest.mark.parametrize( + "processing_delay_seconds, max_retries", + [ + # The record expired while retrying + pytest.param(0, -1, id="expire-while-retrying"), + # The record expired prior to arriving (no retries expected) + pytest.param(60, 0, id="expire-before-ingestion"), + ], + ) + def test_kinesis_maximum_record_age_exceeded( + self, + create_lambda_function, + kinesis_create_stream, + sqs_get_queue_arn, + create_event_source_mapping, + lambda_su_role, + wait_for_stream_ready, + snapshot, + aws_client, + region_name, + sqs_create_queue, + monkeypatch, + # Parametrized arguments + processing_delay_seconds, + max_retries, + ): + # snapshot setup + snapshot.add_transformer(snapshot.transform.key_value("MD5OfBody")) + snapshot.add_transformer(snapshot.transform.key_value("ReceiptHandle")) + snapshot.add_transformer(snapshot.transform.key_value("startSequenceNumber")) + + function_name = f"lambda_func-{short_uid()}" + stream_name = f"test-kinesis-{short_uid()}" + + kinesis_create_stream(StreamName=stream_name, ShardCount=1) + wait_for_stream_ready(stream_name=stream_name) + stream_summary = aws_client.kinesis.describe_stream_summary(StreamName=stream_name) + assert stream_summary["StreamDescriptionSummary"]["OpenShardCount"] == 1 + stream_arn = aws_client.kinesis.describe_stream(StreamName=stream_name)[ + "StreamDescription" + ]["StreamARN"] + + aws_client.kinesis.put_record( + Data="stream-data", + PartitionKey="test", + StreamName=stream_name, + ) + + if processing_delay_seconds > 0: + # Optionally delay the ESM creation, allowing a record to expire prior to being ingested. + time.sleep(processing_delay_seconds) + + create_lambda_function( + handler_file=TEST_LAMBDA_ECHO_FAILURE, + func_name=function_name, + runtime=Runtime.python3_12, + role=lambda_su_role, + ) + + # Use OnFailure config with a DLQ to minimise flakiness instead of relying on Cloudwatch logs + queue_event_source_mapping = sqs_create_queue() + destination_queue = sqs_get_queue_arn(queue_event_source_mapping) + destination_config = {"OnFailure": {"Destination": destination_queue}} + + create_event_source_mapping_response = create_event_source_mapping( + FunctionName=function_name, + BatchSize=1, + StartingPosition="TRIM_HORIZON", + EventSourceArn=stream_arn, + MaximumBatchingWindowInSeconds=1, + MaximumRetryAttempts=max_retries, + MaximumRecordAgeInSeconds=60, + DestinationConfig=destination_config, + ) + snapshot.match("create_event_source_mapping_response", create_event_source_mapping_response) + event_source_mapping_uuid = create_event_source_mapping_response["UUID"] + _await_event_source_mapping_enabled(aws_client.lambda_, event_source_mapping_uuid) + + def _verify_failure_received(): + result = aws_client.sqs.receive_message(QueueUrl=queue_event_source_mapping) + assert result.get("Messages") + return result + + sleep = 15 if is_aws_cloud() else 5 + record_age_exceeded_payload = retry( + _verify_failure_received, retries=30, sleep=sleep, sleep_before=5 + ) + snapshot.match("record_age_exceeded_payload", record_age_exceeded_payload) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # FIXME: Generate and send a requestContext in StreamPoller for RecordAgeExceeded + # which contains no responseContext object. + "$..Messages..Body.requestContext", + "$..Messages..MessageId", # Skip while no requestContext generated in StreamPoller due to transformation issues + ] + ) + def test_kinesis_maximum_record_age_exceeded_discard_records( + self, + create_lambda_function, + kinesis_create_stream, + sqs_get_queue_arn, + create_event_source_mapping, + lambda_su_role, + wait_for_stream_ready, + snapshot, + aws_client, + sqs_create_queue, + ): + # snapshot setup + snapshot.add_transformer(snapshot.transform.key_value("MD5OfBody")) + snapshot.add_transformer(snapshot.transform.key_value("ReceiptHandle")) + snapshot.add_transformer(snapshot.transform.key_value("startSequenceNumber")) + + function_name = f"lambda_func-{short_uid()}" + stream_name = f"test-kinesis-{short_uid()}" + + kinesis_create_stream(StreamName=stream_name, ShardCount=1) + wait_for_stream_ready(stream_name=stream_name) + stream_summary = aws_client.kinesis.describe_stream_summary(StreamName=stream_name) + assert stream_summary["StreamDescriptionSummary"]["OpenShardCount"] == 1 + stream_arn = aws_client.kinesis.describe_stream(StreamName=stream_name)[ + "StreamDescription" + ]["StreamARN"] + + aws_client.kinesis.put_record( + Data="stream-data", + PartitionKey="test", + StreamName=stream_name, + ) + + # Ensure that the first record has expired + time.sleep(60) + + # The first record in the batch has expired with the remaining batch not exceeding any age-limits. + for i in range(5): + aws_client.kinesis.put_record( + Data=f"stream-data-{i + 1}", + PartitionKey="test", + StreamName=stream_name, + ) + + destination_queue_url = sqs_create_queue() + create_lambda_function( + func_name=function_name, + handler_file=TEST_LAMBDA_EVENT_SOURCE_MAPPING_SEND_MESSAGE, + runtime=Runtime.python3_12, + envvars={"SQS_QUEUE_URL": destination_queue_url}, + role=lambda_su_role, + ) + + # Use OnFailure config with a DLQ to minimise flakiness instead of relying on Cloudwatch logs + dead_letter_queue = sqs_create_queue() + dead_letter_queue_arn = sqs_get_queue_arn(dead_letter_queue) + destination_config = {"OnFailure": {"Destination": dead_letter_queue_arn}} + + create_event_source_mapping_response = create_event_source_mapping( + FunctionName=function_name, + BatchSize=10, + StartingPosition="TRIM_HORIZON", + EventSourceArn=stream_arn, + MaximumBatchingWindowInSeconds=1, + MaximumRetryAttempts=0, + MaximumRecordAgeInSeconds=60, + DestinationConfig=destination_config, + ) + snapshot.match("create_event_source_mapping_response", create_event_source_mapping_response) + event_source_mapping_uuid = create_event_source_mapping_response["UUID"] + _await_event_source_mapping_enabled(aws_client.lambda_, event_source_mapping_uuid) + + def _verify_failure_received(): + result = aws_client.sqs.receive_message(QueueUrl=dead_letter_queue) + assert result.get("Messages") + return result + + batches = [] + + def _verify_events_received(expected: int): + messages_to_delete = [] + receive_message_response = aws_client.sqs.receive_message( + QueueUrl=destination_queue_url, + MaxNumberOfMessages=10, + VisibilityTimeout=120, + WaitTimeSeconds=5 if is_aws_cloud() else 1, + ) + messages = receive_message_response.get("Messages", []) + for message in messages: + received_batch = json.loads(message["Body"]) + batches.append(received_batch) + messages_to_delete.append( + {"Id": message["MessageId"], "ReceiptHandle": message["ReceiptHandle"]} + ) + if messages_to_delete: + aws_client.sqs.delete_message_batch( + QueueUrl=destination_queue_url, Entries=messages_to_delete + ) + assert sum([len(batch) for batch in batches]) == expected + return [message for batch in batches for message in batch] + + sleep = 15 if is_aws_cloud() else 5 + record_age_exceeded_payload = retry( + _verify_failure_received, retries=15, sleep=sleep, sleep_before=5 + ) + snapshot.match("record_age_exceeded_payload", record_age_exceeded_payload) + + # While 6 records were sent, we expect 5 records since the first + # record should have expired and been discarded. + kinesis_events = retry( + _verify_events_received, retries=30, sleep=sleep, sleep_before=5, expected=5 + ) + snapshot.match("Records", kinesis_events) + @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.snapshot.json index ee96a18aa4aa0..809b9f0d539cd 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.snapshot.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.snapshot.json @@ -3173,5 +3173,282 @@ } } } + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded[expire-while-retrying]": { + "recorded-date": "13-04-2025, 15:00:55", + "recorded-content": { + "create_event_source_mapping_response": { + "BatchSize": 1, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": { + "Destination": "arn::sqs::111111111111:" + } + }, + "EventSourceArn": "arn::kinesis::111111111111:stream/", + "EventSourceMappingArn": "arn::lambda::111111111111:event-source-mapping:", + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 1, + "MaximumRecordAgeInSeconds": 60, + "MaximumRetryAttempts": -1, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Creating", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "record_age_exceeded_payload": { + "Messages": [ + { + "Body": { + "requestContext": { + "requestId": "", + "functionArn": "arn::lambda::111111111111:function:", + "condition": "RecordAgeExceeded", + "approximateInvokeCount": 1 + }, + "version": "1.0", + "timestamp": "", + "KinesisBatchInfo": { + "shardId": "shardId-000000000000", + "startSequenceNumber": "", + "endSequenceNumber": "", + "approximateArrivalOfFirstRecord": "", + "approximateArrivalOfLastRecord": "", + "batchSize": 1, + "streamArn": "arn::kinesis::111111111111:stream/" + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded[expire-before-ingestion]": { + "recorded-date": "13-04-2025, 16:29:29", + "recorded-content": { + "create_event_source_mapping_response": { + "BatchSize": 1, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": { + "Destination": "arn::sqs::111111111111:" + } + }, + "EventSourceArn": "arn::kinesis::111111111111:stream/", + "EventSourceMappingArn": "arn::lambda::111111111111:event-source-mapping:", + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 1, + "MaximumRecordAgeInSeconds": 60, + "MaximumRetryAttempts": 0, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Creating", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "record_age_exceeded_payload": { + "Messages": [ + { + "Body": { + "requestContext": { + "requestId": "", + "functionArn": "arn::lambda::111111111111:function:", + "condition": "RecordAgeExceeded", + "approximateInvokeCount": 1 + }, + "version": "1.0", + "timestamp": "", + "KinesisBatchInfo": { + "shardId": "shardId-000000000000", + "startSequenceNumber": "", + "endSequenceNumber": "", + "approximateArrivalOfFirstRecord": "", + "approximateArrivalOfLastRecord": "", + "batchSize": 1, + "streamArn": "arn::kinesis::111111111111:stream/" + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded_discard_records": { + "recorded-date": "13-04-2025, 17:05:16", + "recorded-content": { + "create_event_source_mapping_response": { + "BatchSize": 10, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": { + "Destination": "arn::sqs::111111111111:" + } + }, + "EventSourceArn": "arn::kinesis::111111111111:stream/", + "EventSourceMappingArn": "arn::lambda::111111111111:event-source-mapping:", + "FunctionArn": "arn::lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 1, + "MaximumRecordAgeInSeconds": 60, + "MaximumRetryAttempts": 0, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Creating", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "record_age_exceeded_payload": { + "Messages": [ + { + "Body": { + "requestContext": { + "requestId": "", + "functionArn": "arn::lambda::111111111111:function:", + "condition": "RecordAgeExceeded", + "approximateInvokeCount": 1 + }, + "version": "1.0", + "timestamp": "", + "KinesisBatchInfo": { + "shardId": "shardId-000000000000", + "startSequenceNumber": "", + "endSequenceNumber": "", + "approximateArrivalOfFirstRecord": "", + "approximateArrivalOfLastRecord": "", + "batchSize": 1, + "streamArn": "arn::kinesis::111111111111:stream/" + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "test", + "sequenceNumber": "", + "data": "c3RyZWFtLWRhdGEtMQ==", + "approximateArrivalTimestamp": "" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn::iam::111111111111:role/", + "awsRegion": "", + "eventSourceARN": "arn::kinesis::111111111111:stream/" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "test", + "sequenceNumber": "", + "data": "c3RyZWFtLWRhdGEtMg==", + "approximateArrivalTimestamp": "" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn::iam::111111111111:role/", + "awsRegion": "", + "eventSourceARN": "arn::kinesis::111111111111:stream/" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "test", + "sequenceNumber": "", + "data": "c3RyZWFtLWRhdGEtMw==", + "approximateArrivalTimestamp": "" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn::iam::111111111111:role/", + "awsRegion": "", + "eventSourceARN": "arn::kinesis::111111111111:stream/" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "test", + "sequenceNumber": "", + "data": "c3RyZWFtLWRhdGEtNA==", + "approximateArrivalTimestamp": "" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn::iam::111111111111:role/", + "awsRegion": "", + "eventSourceARN": "arn::kinesis::111111111111:stream/" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "test", + "sequenceNumber": "", + "data": "c3RyZWFtLWRhdGEtNQ==", + "approximateArrivalTimestamp": "" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn::iam::111111111111:role/", + "awsRegion": "", + "eventSourceARN": "arn::kinesis::111111111111:stream/" + } + ] + } } } diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json index ef98dfb806d7e..02855382cbf8e 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json @@ -32,6 +32,18 @@ "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_event_source_trim_horizon": { "last_validated_date": "2024-12-13T14:06:49+00:00" }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded": { + "last_validated_date": "2025-04-13T15:57:25+00:00" + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded[expire-before-ingestion]": { + "last_validated_date": "2025-04-13T16:29:25+00:00" + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded[expire-with-mixed-arrival-batch]": { + "last_validated_date": "2025-04-13T16:39:43+00:00" + }, + "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded_discard_records": { + "last_validated_date": "2025-04-13T17:05:13+00:00" + }, "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_report_batch_item_failure_scenarios[empty_string_item_identifier_failure]": { "last_validated_date": "2024-12-13T14:23:18+00:00" }, diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_sqs.py b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_sqs.py index 1765c404e6107..a3080139ab57f 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_sqs.py +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_sqs.py @@ -1105,10 +1105,10 @@ def get_msg_from_q(): messages_to_delete.append( {"Id": message["MessageId"], "ReceiptHandle": message["ReceiptHandle"]} ) - - aws_client.sqs.delete_message_batch( - QueueUrl=destination_queue_url, Entries=messages_to_delete - ) + if messages_to_delete: + aws_client.sqs.delete_message_batch( + QueueUrl=destination_queue_url, Entries=messages_to_delete + ) assert sum([len(batch) for batch in batches]) == 15 return [message for batch in batches for message in batch] From 10bc97ac208a2073656f7888b1b84361bfaa9c61 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:06:38 +0200 Subject: [PATCH 074/108] Cloud Formation Engine v2: Improve Computation of Ref Functions and PhysicalResourceIDs Listing (#12535) --- .../engine/v2/change_set_model_describer.py | 19 +- .../engine/v2/change_set_model_executor.py | 78 +- .../engine/v2/change_set_model_preproc.py | 75 +- .../services/cloudformation/v2/entities.py | 1 + .../v2/test_change_set_conditions.py | 7 +- .../v2/test_change_set_fn_get_attr.py | 1 - .../v2/test_change_set_mappings.py | 1 - .../cloudformation/v2/test_change_set_ref.py | 1 - .../test_change_set_describe_details.py | 1746 ----------------- 9 files changed, 110 insertions(+), 1819 deletions(-) delete mode 100644 tests/unit/services/cloudformation/test_change_set_describe_details.py diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index 49a07a2b426e8..cf7f4330923c3 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -24,8 +24,15 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc): _include_property_values: Final[bool] _changes: Final[cfn_api.Changes] - def __init__(self, node_template: NodeTemplate, include_property_values: bool): - super().__init__(node_template=node_template) + def __init__( + self, + node_template: NodeTemplate, + before_resolved_resources: dict, + include_property_values: bool, + ): + super().__init__( + node_template=node_template, before_resolved_resources=before_resolved_resources + ) self._include_property_values = include_property_values self._changes = list() @@ -79,6 +86,7 @@ def _register_resource_change( self, logical_id: str, type_: str, + physical_id: Optional[str], before_properties: Optional[PreprocProperties], after_properties: Optional[PreprocProperties], ) -> None: @@ -92,6 +100,8 @@ def _register_resource_change( resource_change["Action"] = action resource_change["LogicalResourceId"] = logical_id resource_change["ResourceType"] = type_ + if physical_id: + resource_change["PhysicalResourceId"] = physical_id if self._include_property_values and before_properties is not None: before_context_properties = {PropertiesKey: before_properties.properties} before_context_properties_json_str = json.dumps(before_context_properties) @@ -116,6 +126,7 @@ def _describe_resource_change( # Register a Modified if changed. self._register_resource_change( logical_id=name, + physical_id=before.physical_resource_id, type_=before.resource_type, before_properties=before.properties, after_properties=after.properties, @@ -126,6 +137,7 @@ def _describe_resource_change( # Register a Removed for the previous type. self._register_resource_change( logical_id=name, + physical_id=before.physical_resource_id, type_=before.resource_type, before_properties=before.properties, after_properties=None, @@ -133,6 +145,7 @@ def _describe_resource_change( # Register a Create for the next type. self._register_resource_change( logical_id=name, + physical_id=None, type_=after.resource_type, before_properties=None, after_properties=after.properties, @@ -141,6 +154,7 @@ def _describe_resource_change( # Case: removal self._register_resource_change( logical_id=name, + physical_id=before.physical_resource_id, type_=before.resource_type, before_properties=before.properties, after_properties=None, @@ -149,6 +163,7 @@ def _describe_resource_change( # Case: addition self._register_resource_change( logical_id=name, + physical_id=None, type_=after.resource_type, before_properties=None, after_properties=after.properties, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index 6bcb424e194b6..4398338d9a9ae 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -1,7 +1,7 @@ import copy import logging import uuid -from typing import Any, Final, Optional +from typing import Final, Optional from localstack.aws.api.cloudformation import ChangeAction, StackStatus from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY @@ -28,14 +28,17 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc): - change_set: Final[ChangeSet] - # TODO: add typing. + _change_set: Final[ChangeSet] + # TODO: add typing for resolved resources and parameters. resources: Final[dict] resolved_parameters: Final[dict] def __init__(self, change_set: ChangeSet): - super().__init__(node_template=change_set.update_graph) - self.change_set = change_set + super().__init__( + node_template=change_set.update_graph, + before_resolved_resources=change_set.stack.resolved_resources, + ) + self._change_set = change_set self.resources = dict() self.resolved_parameters = dict() @@ -49,44 +52,35 @@ def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDe self.resolved_parameters[node_parameter.name] = delta.after return delta + def _after_resource_physical_id(self, resource_logical_id: str) -> Optional[str]: + after_resolved_resources = self.resources + return self._resource_physical_resource_id_from( + logical_resource_id=resource_logical_id, resolved_resources=after_resolved_resources + ) + def visit_node_resource( self, node_resource: NodeResource ) -> PreprocEntityDelta[PreprocResource, PreprocResource]: + """ + Overrides the default preprocessing for NodeResource objects by annotating the + `after` delta with the physical resource ID, if side effects resulted in an update. + """ delta = super().visit_node_resource(node_resource=node_resource) self._execute_on_resource_change( name=node_resource.name, before=delta.before, after=delta.after ) - return delta - - def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> PreprocEntityDelta: - if not isinstance(preproc_value, PreprocResource): - return super()._reduce_intrinsic_function_ref_value(preproc_value=preproc_value) - - logical_id = preproc_value.name - - def _get_physical_id_of_resolved_resource(resolved_resource: dict) -> str: - physical_resource_id = resolved_resource.get("PhysicalResourceId") - if not isinstance(physical_resource_id, str): + after_resource = delta.after + if after_resource is not None and delta.before != delta.after: + after_logical_id = after_resource.logical_id + after_physical_id: Optional[str] = self._after_resource_physical_id( + resource_logical_id=after_logical_id + ) + if after_physical_id is None: raise RuntimeError( - f"No physical resource id found for resource '{logical_id}' during ChangeSet execution" + f"No PhysicalResourceId was found for resource '{after_physical_id}' post-update." ) - return physical_resource_id - - before_resolved_resources = self.change_set.stack.resolved_resources - after_resolved_resources = self.resources - - before_physical_id = None - if logical_id in before_resolved_resources: - before_resolved_resource = before_resolved_resources[logical_id] - before_physical_id = _get_physical_id_of_resolved_resource(before_resolved_resource) - after_physical_id = None - if logical_id in after_resolved_resources: - after_resolved_resource = after_resolved_resources[logical_id] - after_physical_id = _get_physical_id_of_resolved_resource(after_resolved_resource) - - if before_physical_id is None and after_physical_id is None: - raise RuntimeError(f"No resource '{logical_id}' found during ChangeSet execution") - return PreprocEntityDelta(before=before_physical_id, after=after_physical_id) + after_resource.physical_resource_id = after_physical_id + return delta def _execute_on_resource_change( self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] @@ -156,7 +150,7 @@ def _execute_on_resource_change( def _merge_before_properties( self, name: str, preproc_resource: PreprocResource ) -> PreprocProperties: - if previous_resource_properties := self.change_set.stack.resolved_resources.get( + if previous_resource_properties := self._change_set.stack.resolved_resources.get( name, {} ).get("Properties"): return PreprocProperties(properties=previous_resource_properties) @@ -174,7 +168,7 @@ def _execute_resource_action( ) -> None: LOG.debug("Executing resource action: %s for resource '%s'", action, logical_resource_id) resource_provider_executor = ResourceProviderExecutor( - stack_name=self.change_set.stack.stack_name, stack_id=self.change_set.stack.stack_id + stack_name=self._change_set.stack.stack_name, stack_id=self._change_set.stack.stack_id ) payload = self.create_resource_provider_payload( action=action, @@ -199,7 +193,7 @@ def _execute_resource_action( reason, exc_info=LOG.isEnabledFor(logging.DEBUG), ) - stack = self.change_set.stack + stack = self._change_set.stack stack_status = stack.status if stack_status == StackStatus.CREATE_IN_PROGRESS: stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) @@ -226,7 +220,7 @@ def _execute_resource_action( reason, ) # TODO: duplication - stack = self.change_set.stack + stack = self._change_set.stack stack_status = stack.status if stack_status == StackStatus.CREATE_IN_PROGRESS: stack.set_stack_status(StackStatus.CREATE_FAILED, reason=reason) @@ -247,7 +241,7 @@ def create_resource_provider_payload( ) -> Optional[ResourceProviderPayload]: # FIXME: use proper credentials creds: Credentials = { - "accessKeyId": self.change_set.stack.account_id, + "accessKeyId": self._change_set.stack.account_id, "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY, "sessionToken": "", } @@ -268,14 +262,14 @@ def create_resource_provider_payload( raise NotImplementedError(f"Action '{action}' not handled") resource_provider_payload: ResourceProviderPayload = { - "awsAccountId": self.change_set.stack.account_id, + "awsAccountId": self._change_set.stack.account_id, "callbackContext": {}, - "stackId": self.change_set.stack.stack_name, + "stackId": self._change_set.stack.stack_name, "resourceType": resource_type, "resourceTypeVersion": "000000", # TODO: not actually a UUID "bearerToken": str(uuid.uuid4()), - "region": self.change_set.stack.region_name, + "region": self._change_set.stack.region_name, "action": str(action), "requestData": { "logicalResourceId": logical_resource_id, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index 025aeadc52c18..6c24510f2bef4 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -61,20 +61,23 @@ def __eq__(self, other): class PreprocResource: - name: str + logical_id: str + physical_resource_id: Optional[str] condition: Optional[bool] resource_type: str properties: PreprocProperties def __init__( self, - name: str, + logical_id: str, + physical_resource_id: str, condition: Optional[bool], resource_type: str, properties: PreprocProperties, ): + self.logical_id = logical_id + self.physical_resource_id = physical_resource_id self.condition = condition - self.name = name self.resource_type = resource_type self.properties = properties @@ -90,7 +93,7 @@ def __eq__(self, other): return False return all( [ - self.name == other.name, + self.logical_id == other.logical_id, self._compare_conditions(self.condition, other.condition), self.resource_type == other.resource_type, self.properties == other.properties, @@ -125,10 +128,12 @@ def __eq__(self, other): class ChangeSetModelPreproc(ChangeSetModelVisitor): _node_template: Final[NodeTemplate] + _before_resolved_resources: Final[dict] _processed: dict[Scope, Any] - def __init__(self, node_template: NodeTemplate): + def __init__(self, node_template: NodeTemplate, before_resolved_resources: dict): self._node_template = node_template + self._before_resolved_resources = before_resolved_resources self._processed = dict() def process(self) -> None: @@ -411,35 +416,49 @@ def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDe delta = self.visit(node_condition.body) return delta - def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> PreprocEntityDelta: - if isinstance(preproc_value, PreprocResource): - value = preproc_value.name - else: - value = preproc_value - return PreprocEntityDelta(value, value) + def _resource_physical_resource_id_from( + self, logical_resource_id: str, resolved_resources: dict + ) -> Optional[str]: + # TODO: typing around resolved resources is needed and should be reflected here. + resolved_resource = resolved_resources.get(logical_resource_id) + if resolved_resource is None: + return None + physical_resource_id: Optional[str] = resolved_resource.get("PhysicalResourceId") + if not isinstance(physical_resource_id, str): + raise RuntimeError(f"No PhysicalResourceId found for resource '{logical_resource_id}'") + return physical_resource_id + + def _before_resource_physical_id(self, resource_logical_id: str) -> Optional[str]: + # TODO: typing around resolved resources is needed and should be reflected here. + return self._resource_physical_resource_id_from( + logical_resource_id=resource_logical_id, + resolved_resources=self._before_resolved_resources, + ) + + def _after_resource_physical_id(self, resource_logical_id: str) -> Optional[str]: + return self._before_resource_physical_id(resource_logical_id=resource_logical_id) def visit_node_intrinsic_function_ref( self, node_intrinsic_function: NodeIntrinsicFunction ) -> PreprocEntityDelta: arguments_delta = self.visit(node_intrinsic_function.arguments) - - # TODO: add tests with created and deleted parameters and verify this logic holds. before_logical_id = arguments_delta.before + after_logical_id = arguments_delta.after + + # TODO: extend this to support references to other types. before = None if before_logical_id is not None: before_delta = self._resolve_reference(logical_id=before_logical_id) - before_value = before_delta.before - before_ref_delta = self._reduce_intrinsic_function_ref_value(before_value) - before = before_ref_delta.before + before = before_delta.before + if isinstance(before, PreprocResource): + before = before.physical_resource_id - after_logical_id = arguments_delta.after after = None if after_logical_id is not None: after_delta = self._resolve_reference(logical_id=after_logical_id) - after_value = after_delta.after - # TODO: swap isinstance to be a structured type check - after_ref_delta = self._reduce_intrinsic_function_ref_value(after_value) - after = after_ref_delta.after + after = after_delta.after + if isinstance(after, PreprocResource): + after = after.physical_resource_id return PreprocEntityDelta(before=before, after=after) @@ -505,15 +524,25 @@ def visit_node_resource( before = None after = None if change_type != ChangeType.CREATED and condition_before is None or condition_before: + logical_resource_id = node_resource.name + before_physical_resource_id = self._before_resource_physical_id( + resource_logical_id=logical_resource_id + ) before = PreprocResource( - name=node_resource.name, + logical_id=logical_resource_id, + physical_resource_id=before_physical_resource_id, condition=condition_before, resource_type=type_delta.before, properties=properties_delta.before, ) if change_type != ChangeType.REMOVED and condition_after is None or condition_after: + logical_resource_id = node_resource.name + after_physical_resource_id = self._after_resource_physical_id( + resource_logical_id=logical_resource_id + ) after = PreprocResource( - name=node_resource.name, + logical_id=logical_resource_id, + physical_resource_id=after_physical_resource_id, condition=condition_after, resource_type=type_delta.after, properties=properties_delta.after, diff --git a/localstack-core/localstack/services/cloudformation/v2/entities.py b/localstack-core/localstack/services/cloudformation/v2/entities.py index ae9af9ad2ec9f..b73d33d917783 100644 --- a/localstack-core/localstack/services/cloudformation/v2/entities.py +++ b/localstack-core/localstack/services/cloudformation/v2/entities.py @@ -173,6 +173,7 @@ def populate_update_graph( def describe_details(self, include_property_values: bool) -> DescribeChangeSetOutput: change_set_describer = ChangeSetModelDescriber( node_template=self.update_graph, + before_resolved_resources=self.stack.resolved_resources, include_property_values=include_property_values, ) changes: Changes = change_set_describer.get_changes() diff --git a/tests/aws/services/cloudformation/v2/test_change_set_conditions.py b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py index 1b597df290238..69312a633fd04 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_conditions.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py @@ -22,14 +22,15 @@ "$..Parameters", "$..Replacement", "$..PolicyAction", - "$..PhysicalResourceId", ] ) class TestChangeSetConditions: @markers.aws.validated @pytest.mark.skip( - reason="The inclusion of response parameters in executor is in progress, " - "currently it cannot delete due to missing topic arn in the request" + reason=( + "The inclusion of response parameters in executor is in progress, " + "currently it cannot delete due to missing topic arn in the request" + ) ) def test_condition_update_removes_resource( self, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py index c4c9954eee9c1..91fa1122aa4b3 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py @@ -22,7 +22,6 @@ "$..Parameters", "$..Replacement", "$..PolicyAction", - "$..PhysicalResourceId", ] ) class TestChangeSetFnGetAttr: diff --git a/tests/aws/services/cloudformation/v2/test_change_set_mappings.py b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py index fd25328225e41..d6d4573ac80ee 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_mappings.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py @@ -22,7 +22,6 @@ "$..Parameters", "$..Replacement", "$..PolicyAction", - "$..PhysicalResourceId", ] ) class TestChangeSetMappings: diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.py b/tests/aws/services/cloudformation/v2/test_change_set_ref.py index 01f90058aa3c5..515cf3c967bd6 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_ref.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.py @@ -22,7 +22,6 @@ "$..Parameters", "$..Replacement", "$..PolicyAction", - "$..PhysicalResourceId", ] ) class TestChangeSetRef: diff --git a/tests/unit/services/cloudformation/test_change_set_describe_details.py b/tests/unit/services/cloudformation/test_change_set_describe_details.py deleted file mode 100644 index ea8b79939dfd9..0000000000000 --- a/tests/unit/services/cloudformation/test_change_set_describe_details.py +++ /dev/null @@ -1,1746 +0,0 @@ -import json -from typing import Optional - -import pytest - -from localstack.aws.api.cloudformation import Changes -from localstack.services.cloudformation.engine.v2.change_set_model import ( - ChangeSetModel, - ChangeType, - NodeOutput, - NodeTemplate, -) -from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( - ChangeSetModelDescriber, -) -from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( - ChangeSetModelPreproc, - PreprocEntityDelta, - PreprocOutput, -) - - -# TODO: the following is used to debug the logic of the ChangeSetModelPreproc in the -# following temporary test suite. This logic should be removed and the tests on output -# management ported to integration tests using the snapshot strategy. -class DebugOutputPreProc(ChangeSetModelPreproc): - outputs_before: list[dict] - outputs_after: list[dict] - - def __init__(self, node_template: NodeTemplate): - super().__init__(node_template=node_template) - self.outputs_before = list() - self.outputs_after = list() - - @staticmethod - def _to_debug_output(change_type: ChangeType, preproc_output: PreprocOutput) -> dict: - debug_object = { - "ChangeType": change_type.value, - "Name": preproc_output.name, - "Value": preproc_output.value, - } - if preproc_output.condition: - debug_object["Condition"] = preproc_output.condition - if preproc_output.export: - debug_object["Export"] = preproc_output.export - return debug_object - - def visit_node_output( - self, node_output: NodeOutput - ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]: - delta = super().visit_node_output(node_output) - if delta.before: - debug_object = self._to_debug_output(node_output.change_type, delta.before) - self.outputs_before.append(debug_object) - if delta.after: - debug_object = self._to_debug_output(node_output.change_type, delta.after) - self.outputs_after.append(debug_object) - return delta - - -# TODO: this is a temporary test suite for the v2 CFN update engine change set description logic. -# should be replaced in favour of v2 integration tests. -class TestChangeSetDescribeDetails: - @staticmethod - def eval_change_set( - before_template: dict, - after_template: dict, - before_parameters: Optional[dict] = None, - after_parameters: Optional[dict] = None, - ) -> Changes: - change_set_model = ChangeSetModel( - before_template=before_template, - after_template=after_template, - before_parameters=before_parameters, - after_parameters=after_parameters, - ) - update_model: NodeTemplate = change_set_model.get_update_model() - change_set_describer = ChangeSetModelDescriber( - node_template=update_model, include_property_values=True - ) - changes = change_set_describer.get_changes() - for change in changes: - resource_change = change["ResourceChange"] - before_context_str = resource_change.get("BeforeContext") - if before_context_str is not None: - resource_change["BeforeContext"] = json.loads(before_context_str) - after_context_str = resource_change.get("AfterContext") - if after_context_str is not None: - resource_change["AfterContext"] = json.loads(after_context_str) - json_str = json.dumps(changes) - return json.loads(json_str) - - @staticmethod - def debug_output_preproc( - before_template: Optional[dict], - after_template: Optional[dict], - before_parameters: Optional[dict] = None, - after_parameters: Optional[dict] = None, - ) -> tuple[list[dict], list[dict]]: - change_set_model = ChangeSetModel( - before_template=before_template, - after_template=after_template, - before_parameters=before_parameters, - after_parameters=after_parameters, - ) - update_model: NodeTemplate = change_set_model.get_update_model() - preproc_output = DebugOutputPreProc(update_model) - preproc_output.visit(update_model.outputs) - return preproc_output.outputs_before, preproc_output.outputs_after - - @staticmethod - def compare_changes(computed: list, target: list) -> None: - def sort_criteria(resource_change): - return resource_change["ResourceChange"]["LogicalResourceId"] - - assert sorted(computed, key=sort_criteria) == sorted(target, key=sort_criteria) - - def test_direct_update(self): - t1 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": "topic-1", - }, - }, - }, - } - t2 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": "topic-2", - }, - }, - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": {"Properties": {"TopicName": "topic-2"}}, - "BeforeContext": {"Properties": {"TopicName": "topic-1"}}, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "topic-2-fdd551f7", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "topic-1-eaed84b9", - # "Name": "TopicName", - # "Path": "/Properties/TopicName", - # "RequiresRecreation": "Always" - # } - # } - # ], - "LogicalResourceId": "Foo", - # "PhysicalResourceId": "arn::sns::111111111111:topic-1", - # "PolicyAction": "ReplaceAndDelete", - # "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - } - ] - self.compare_changes(changes, target) - - def test_dynamic_update(self): - t1 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": "topic-1", - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - t2 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": "topic-2", - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": {"Properties": {"TopicName": "topic-2"}}, - "BeforeContext": {"Properties": {"TopicName": "topic-1"}}, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "topic-2-6da2c5b0", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "topic-1-1601f61d", - # "Name": "TopicName", - # "Path": "/Properties/TopicName", - # "RequiresRecreation": "Always" - # } - # } - # ], - "LogicalResourceId": "Foo", - # "PhysicalResourceId": "arn::sns::111111111111:topic-1", - # "PolicyAction": "ReplaceAndDelete", - # "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": {"Value": "{{changeSet:KNOWN_AFTER_APPLY}}", "Type": "String"} - }, - "BeforeContext": {"Properties": {"Value": "topic-1", "Type": "String"}}, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Dynamic", - # "Target": { - # "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "topic-1-1601f61d", - # "Name": "Value", - # "Path": "/Properties/Value", - # "RequiresRecreation": "Never" - # } - # }, - # { - # "CausingEntity": "Foo.TopicName", - # "ChangeSource": "ResourceAttribute", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "topic-1-1601f61d", - # "Name": "Value", - # "Path": "/Properties/Value", - # "RequiresRecreation": "Never" - # } - # } - # ], - "LogicalResourceId": "Parameter", - # "PhysicalResourceId": "CFN-Parameter", - # "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - }, - ] - self.compare_changes(changes, target) - - def test_unrelated_changes_update_propagation(self): - t1 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "topic_name", - "Description": "original", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - t2 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "topic_name", - "Description": "changed", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "topic_name", - "Type": "String", - "Description": "changed", - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic_name", - "Type": "String", - "Description": "original", - } - }, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "changed", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "original", - # "Name": "Description", - # "Path": "/Properties/Description", - # "RequiresRecreation": "Never" - # } - # } - # ], - "LogicalResourceId": "Parameter1", - # "PhysicalResourceId": "CFN-Parameter1", - # "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - } - ] - self.compare_changes(changes, target) - - @pytest.mark.skip( - reason=( - "Updating an SSN name seems to require replacement of the resource which " - "means the other resource using Fn::GetAtt is known after apply." - ) - ) - def test_unrelated_changes_requires_replacement(self): - t1 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "MyParameter-1", - "Type": "String", - "Value": "value", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - t2 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "MyParameter-2", - "Type": "String", - "Value": "value", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": {"Value": "value", "Type": "String", "Name": "MyParameter-2"} - }, - "BeforeContext": { - "Properties": {"Value": "value", "Type": "String", "Name": "MyParameter-1"} - }, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "MyParameter846966c8", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "MyParameter676af33a", - # "Name": "Name", - # "Path": "/Properties/Name", - # "RequiresRecreation": "Always" - # } - # } - # ], - "LogicalResourceId": "Parameter1", - # "PhysicalResourceId": "MyParameter676af33a", - # "PolicyAction": "ReplaceAndDelete", - # "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": {"Value": "{{changeSet:KNOWN_AFTER_APPLY}}", "Type": "String"} - }, - "BeforeContext": {"Properties": {"Value": "value", "Type": "String"}}, - # "Details": [ - # { - # "ChangeSource": "DirectModification", - # "Evaluation": "Dynamic", - # "Target": { - # "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "value", - # "Name": "Value", - # "Path": "/Properties/Value", - # "RequiresRecreation": "Never" - # } - # }, - # { - # "CausingEntity": "Parameter1.Value", - # "ChangeSource": "ResourceAttribute", - # "Evaluation": "Static", - # "Target": { - # "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - # "Attribute": "Properties", - # "AttributeChangeType": "Modify", - # "BeforeValue": "value", - # "Name": "Value", - # "Path": "/Properties/Value", - # "RequiresRecreation": "Never" - # } - # } - # ], - "LogicalResourceId": "Parameter2", - # "PhysicalResourceId": "CFN-Parameter2", - # "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [ - # "Properties" - # ] - }, - "Type": "Resource", - }, - ] - self.compare_changes(changes, target) - - def test_parameters_dynamic_change(self): - t1 = { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - } - }, - } - changes = self.eval_change_set( - t1, t1, {"ParameterValue": "value-1"}, {"ParameterValue": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "Parameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "55252c2c", - # "AfterValue": "f8679c0b", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Dynamic", - # "ChangeSource": "DirectModification" - # }, - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "55252c2c", - # "AfterValue": "f8679c0b", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "ParameterReference", - # "CausingEntity": "ParameterValue" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_parameter_dynamic_change_unrelated_property(self): - t1 = { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "param-name", - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, - }, - }, - }, - } - changes = self.eval_change_set( - t1, t1, {"ParameterValue": "value-1"}, {"ParameterValue": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "Parameter1", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "49f3de25", - # "AfterValue": "0e788b5d", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "ParameterReference", - # "CausingEntity": "ParameterValue" - # }, - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "49f3de25", - # "AfterValue": "0e788b5d", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Dynamic", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": { - "Properties": {"Name": "param-name", "Value": "value-1", "Type": "String"} - }, - "AfterContext": { - "Properties": {"Name": "param-name", "Value": "value-2", "Type": "String"} - }, - }, - } - ] - self.compare_changes(changes, target) - - def test_parameter_dynamic_change_unrelated_property_not_create_only(self): - t1 = { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, - }, - }, - }, - } - changes = self.eval_change_set( - t1, t1, {"ParameterValue": "value-1"}, {"ParameterValue": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "Parameter1", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "d45ab5ec", - # "AfterValue": "c77f207c", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Dynamic", - # "ChangeSource": "DirectModification" - # }, - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "d45ab5ec", - # "AfterValue": "c77f207c", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "ParameterReference", - # "CausingEntity": "ParameterValue" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_parameter_root_change(self): - t1 = { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, - }, - }, - }, - } - changes = self.eval_change_set( - t1, t1, {"ParameterValue": "value-1"}, {"ParameterValue": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "Parameter1", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "d45ab5ec", - # "AfterValue": "c77f207c", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Dynamic", - # "ChangeSource": "DirectModification" - # }, - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "d45ab5ec", - # "AfterValue": "c77f207c", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "ParameterReference", - # "CausingEntity": "ParameterValue" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_parameter_delete_resource(self): - t1 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "Default": "value-1", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-1"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - changes = self.eval_change_set( - t1, t1, {"CreateParameter": "value-1"}, {"CreateParameter": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - # "PolicyAction": "Delete", - "Action": "Remove", - "LogicalResourceId": "SSMParameter2", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [], - # "Details": [], - "BeforeContext": {"Properties": {"Value": "first", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_parameter_create_resource(self): - t1 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "Default": "value-1", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-2"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - changes = self.eval_change_set( - t1, t1, {"CreateParameter": "value-1"}, {"CreateParameter": "value-2"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Add", - "LogicalResourceId": "SSMParameter2", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "True", - # "Scope": [], - # "Details": [], - "AfterContext": {"Properties": {"Value": "first", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_update_create_resource(self): - t1 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-2"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - t2 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-1"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - changes = self.eval_change_set( - t1, t2, {"CreateParameter": "value-1"}, {"CreateParameter": "value-1"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Add", - "LogicalResourceId": "SSMParameter2", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "True", - # "Scope": [], - # "Details": [], - "AfterContext": {"Properties": {"Value": "first", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_update_delete_resource(self): - t1 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-1"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - t2 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-2"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - changes = self.eval_change_set( - t1, t2, {"CreateParameter": "value-1"}, {"CreateParameter": "value-1"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - # "PolicyAction": "Delete", - "Action": "Remove", - "LogicalResourceId": "SSMParameter2", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [], - # "Details": [], - "BeforeContext": {"Properties": {"Value": "first", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_bound_property_assignment_parameter_modified(self): - t1 = { - "Parameters": { - "UseProductionValue": { - "Type": "String", - "AllowedValues": ["true", "false"], - "Default": "false", - } - }, - "Conditions": {"IsProduction": {"Fn::Equals": [{"Ref": "UseProductionValue"}, "true"]}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::If": [ - "IsProduction", - "ProductionParameterValue", - "StagingParameterValue", - ] - }, - }, - } - }, - } - changes = self.eval_change_set( - t1, t1, {"UseProductionValue": "false"}, {"UseProductionValue": "true"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "MySSMParameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "StagingParameterValue", - # "AfterValue": "ProductionParameterValue", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": { - "Properties": {"Value": "StagingParameterValue", "Type": "String"} - }, - "AfterContext": { - "Properties": {"Value": "ProductionParameterValue", "Type": "String"} - }, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_bound_property_assignment_modified(self): - t1 = { - "Parameters": { - "UseProductionValue": { - "Type": "String", - "AllowedValues": ["true", "false"], - "Default": "false", - } - }, - "Conditions": {"IsProduction": {"Fn::Equals": [{"Ref": "UseProductionValue"}, "true"]}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::If": [ - "IsProduction", - "ProductionParameterValue", - "StagingParameterValue", - ] - }, - }, - } - }, - } - t2 = { - "Parameters": { - "UseProductionValue": { - "Type": "String", - "AllowedValues": ["true", "false"], - "Default": "false", - } - }, - "Conditions": { - "IsProduction": {"Fn::Equals": [{"Ref": "UseProductionValue"}, "false"]} - }, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::If": [ - "IsProduction", - "ProductionParameterValue", - "StagingParameterValue", - ] - }, - }, - } - }, - } - changes = self.eval_change_set( - t1, t2, {"UseProductionValue": "false"}, {"UseProductionValue": "false"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "MySSMParameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "StagingParameterValue", - # "AfterValue": "ProductionParameterValue", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": { - "Properties": {"Value": "StagingParameterValue", "Type": "String"} - }, - "AfterContext": { - "Properties": {"Value": "ProductionParameterValue", "Type": "String"} - }, - }, - } - ] - self.compare_changes(changes, target) - - def test_condition_update_production_remove_resource(self): - t1 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": {"Fn::Equals": [{"Ref": "CreateParameter"}, "value-1"]} - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - t2 = { - "Parameters": { - "CreateParameter": { - "Type": "String", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": { - "Fn::Not": [{"Fn::Equals": [{"Ref": "CreateParameter"}, "value-1"]}] - } - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - } - changes = self.eval_change_set( - t1, t2, {"CreateParameter": "value-1"}, {"CreateParameter": "value-1"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - # "PolicyAction": "Delete", - "Action": "Remove", - "LogicalResourceId": "SSMParameter2", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Scope": [], - # "Details": [], - "BeforeContext": {"Properties": {"Value": "first", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_mappings_update_string_referencing_resource(self): - t1 = { - "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] - }, - }, - } - }, - } - t2 = { - "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-2"}}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] - }, - }, - } - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "MySSMParameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "value-1", - # "AfterValue": "value-2", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-2", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_mappings_update_type_referencing_resource(self): - t1 = { - "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] - }, - }, - } - }, - } - t2 = { - "Mappings": { - "GenericMapping": {"EnvironmentA": {"ParameterValue": ["value-1", "value-2"]}} - }, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": ["GenericMapping", "EnvironmentA", "ParameterValue"] - }, - }, - } - }, - } - changes = self.eval_change_set(t1, t2) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "MySSMParameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "value-1", - # "AfterValue": "[value-1, value-2]", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": { - "Properties": {"Value": ["value-1", "value-2"], "Type": "String"} - }, - }, - } - ] - self.compare_changes(changes, target) - - @pytest.mark.skip(reason="Add support for nested intrinsic functions") - def test_mappings_update_referencing_resource_through_parameter(self): - t1 = { - "Parameters": { - "Environment": { - "Type": "String", - "AllowedValues": [ - "EnvironmentA", - ], - } - }, - "Mappings": { - "GenericMapping": { - "EnvironmentA": {"ParameterValue": "value-1"}, - "EnvironmentB": {"ParameterValue": "value-2"}, - } - }, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": [ - "GenericMapping", - {"Ref": "Environment"}, - "ParameterValue", - ] - }, - }, - } - }, - } - t2 = { - "Parameters": { - "Environment": { - "Type": "String", - "AllowedValues": ["EnvironmentA", "EnvironmentB"], - "Default": "EnvironmentA", - } - }, - "Mappings": { - "GenericMapping": { - "EnvironmentA": {"ParameterValue": "value-1-2"}, - "EnvironmentB": {"ParameterValue": "value-2"}, - } - }, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": [ - "GenericMapping", - {"Ref": "Environment"}, - "ParameterValue", - ] - }, - }, - } - }, - } - changes = self.eval_change_set( - t1, t2, {"Environment": "EnvironmentA"}, {"Environment": "EnvironmentA"} - ) - target = [ - { - "Type": "Resource", - "ResourceChange": { - "Action": "Modify", - "LogicalResourceId": "MySSMParameter", - # "PhysicalResourceId": "", - "ResourceType": "AWS::SSM::Parameter", - # "Replacement": "False", - # "Scope": [ - # "Properties" - # ], - # "Details": [ - # { - # "Target": { - # "Attribute": "Properties", - # "Name": "Value", - # "RequiresRecreation": "Never", - # "Path": "/Properties/Value", - # "BeforeValue": "value-1", - # "AfterValue": "value-1-2", - # "AttributeChangeType": "Modify" - # }, - # "Evaluation": "Static", - # "ChangeSource": "DirectModification" - # } - # ], - "BeforeContext": {"Properties": {"Value": "value-1", "Type": "String"}}, - "AfterContext": {"Properties": {"Value": "value-1-2", "Type": "String"}}, - }, - } - ] - self.compare_changes(changes, target) - - def test_output_new_resource_and_output(self): - t1 = { - "Resources": { - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - } - } - } - t2 = { - "Resources": { - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - "NewParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "param-name", "Type": "String", "Value": "value-1"}, - }, - }, - "Outputs": {"NewParamName": {"Value": {"Ref": "NewParam"}}}, - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. - assert not outputs_before - assert outputs_after == [ - {"ChangeType": "Created", "Name": "NewParamName", "Value": "NewParam"} - ] - - def test_output_and_resource_removed(self): - t1 = { - "Resources": { - "FeatureToggle": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "app-feature-toggle", - "Type": "String", - "Value": "enabled", - }, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"FeatureToggleName": {"Value": {"Ref": "FeatureToggle"}}}, - } - t2 = { - "Resources": { - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - } - } - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. - assert outputs_before == [ - {"ChangeType": "Removed", "Name": "FeatureToggleName", "Value": "FeatureToggle"} - ] - assert outputs_after == [] - - def test_output_resource_changed(self): - t1 = { - "Resources": { - "LogLevelParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "app-log-level", "Type": "String", "Value": "info"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"LogLevelOutput": {"Value": {"Ref": "LogLevelParam"}}}, - } - t2 = { - "Resources": { - "LogLevelParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "app-log-level", "Type": "String", "Value": "debug"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"LogLevelOutput": {"Value": {"Ref": "LogLevelParam"}}}, - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - assert outputs_before == [ - {"ChangeType": "Modified", "Name": "LogLevelOutput", "Value": "LogLevelParam"} - ] - assert outputs_after == [ - {"ChangeType": "Modified", "Name": "LogLevelOutput", "Value": "LogLevelParam"} - ] - - def test_output_update(self): - t1 = { - "Resources": { - "EnvParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "app-env", "Type": "String", "Value": "prod"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"EnvParamRef": {"Value": {"Ref": "EnvParam"}}}, - } - - t2 = { - "Resources": { - "EnvParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "app-env", "Type": "String", "Value": "prod"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"EnvParamRef": {"Value": {"Fn::GetAtt": ["EnvParam", "Name"]}}}, - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. - assert outputs_before == [ - {"ChangeType": "Modified", "Name": "EnvParamRef", "Value": "EnvParam"} - ] - assert outputs_after == [ - {"ChangeType": "Modified", "Name": "EnvParamRef", "Value": "app-env"} - ] - - def test_output_renamed(self): - t1 = { - "Resources": { - "SSMParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "some-param", "Type": "String", "Value": "value"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"OldSSMOutput": {"Value": {"Ref": "SSMParam"}}}, - } - t2 = { - "Resources": { - "SSMParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "some-param", "Type": "String", "Value": "value"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"NewSSMOutput": {"Value": {"Ref": "SSMParam"}}}, - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - assert outputs_before == [ - {"ChangeType": "Removed", "Name": "OldSSMOutput", "Value": "SSMParam"} - ] - assert outputs_after == [ - {"ChangeType": "Created", "Name": "NewSSMOutput", "Value": "SSMParam"} - ] - - def test_output_and_resource_renamed(self): - t1 = { - "Resources": { - "DBPasswordParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "db-password", "Type": "String", "Value": "secret"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"DBPasswordOutput": {"Value": {"Ref": "DBPasswordParam"}}}, - } - t2 = { - "Resources": { - "DatabaseSecretParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "db-password", "Type": "String", "Value": "secret"}, - }, - "UnrelatedParam": { - "Type": "AWS::SSM::Parameter", - "Properties": {"Name": "unrelated-param", "Type": "String", "Value": "foo"}, - }, - }, - "Outputs": {"DatabaseSecretOutput": {"Value": {"Ref": "DatabaseSecretParam"}}}, - } - outputs_before, outputs_after = self.debug_output_preproc(t1, t2) - # NOTE: Outputs are currently evaluated by the describer using the entity name as a proxy, - # as the executor logic is not yet implemented. - assert outputs_before == [ - {"ChangeType": "Removed", "Name": "DBPasswordOutput", "Value": "DBPasswordParam"} - ] - assert outputs_after == [ - { - "ChangeType": "Created", - "Name": "DatabaseSecretOutput", - "Value": "DatabaseSecretParam", - } - ] From 2ed4e11c5f2ca17cf8d7f5bca46e608495e08ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= <18080804+pinzon@users.noreply.github.com> Date: Wed, 23 Apr 2025 08:54:11 -0500 Subject: [PATCH 075/108] fix IAM.SimulatePrincipalPolicy (#12542) --- .../localstack/services/iam/provider.py | 69 +++++- tests/aws/services/iam/test_iam.py | 91 +++++-- tests/aws/services/iam/test_iam.snapshot.json | 234 ++++++++++++++++++ .../aws/services/iam/test_iam.validation.json | 9 + 4 files changed, 370 insertions(+), 33 deletions(-) diff --git a/localstack-core/localstack/services/iam/provider.py b/localstack-core/localstack/services/iam/provider.py index 26da2c8adbe21..312a2a714aafc 100644 --- a/localstack-core/localstack/services/iam/provider.py +++ b/localstack-core/localstack/services/iam/provider.py @@ -107,6 +107,55 @@ def get_iam_backend(context: RequestContext) -> IAMBackend: return iam_backends[context.account_id][context.partition] +def get_policies_from_principal(backend: IAMBackend, principal_arn: str) -> list[dict]: + policies = [] + if ":role" in principal_arn: + role_name = principal_arn.split("/")[-1] + + policies.append(backend.get_role(role_name=role_name).assume_role_policy_document) + + policy_names = backend.list_role_policies(role_name=role_name) + policies.extend( + [ + backend.get_role_policy(role_name=role_name, policy_name=policy_name)[1] + for policy_name in policy_names + ] + ) + + attached_policies, _ = backend.list_attached_role_policies(role_name=role_name) + policies.extend([policy.document for policy in attached_policies]) + + if ":group" in principal_arn: + print(principal_arn) + group_name = principal_arn.split("/")[-1] + policy_names = backend.list_group_policies(group_name=group_name) + policies.extend( + [ + backend.get_group_policy(group_name=group_name, policy_name=policy_name)[1] + for policy_name in policy_names + ] + ) + + attached_policies, _ = backend.list_attached_group_policies(group_name=group_name) + policies.extend([policy.document for policy in attached_policies]) + + if ":user" in principal_arn: + print(principal_arn) + user_name = principal_arn.split("/")[-1] + policy_names = backend.list_user_policies(user_name=user_name) + policies.extend( + [ + backend.get_user_policy(user_name=user_name, policy_name=policy_name)[1] + for policy_name in policy_names + ] + ) + + attached_policies, _ = backend.list_attached_user_policies(user_name=user_name) + policies.extend([policy.document for policy in attached_policies]) + + return policies + + class IamProvider(IamApi): def __init__(self): apply_iam_patches() @@ -168,12 +217,20 @@ def simulate_principal_policy( **kwargs, ) -> SimulatePolicyResponse: backend = get_iam_backend(context) - policy = backend.get_policy(policy_source_arn) - policy_version = backend.get_policy_version(policy_source_arn, policy.default_version_id) - try: - policy_statements = json.loads(policy_version.document).get("Statement", []) - except Exception: - raise NoSuchEntityException("Policy not found") + + policies = get_policies_from_principal(backend, policy_source_arn) + + def _get_statements_from_policy_list(policies: list[str]): + statements = [] + for policy_str in policies: + policy_dict = json.loads(policy_str) + if isinstance(policy_dict["Statement"], list): + statements.extend(policy_dict["Statement"]) + else: + statements.append(policy_dict["Statement"]) + return statements + + policy_statements = _get_statements_from_policy_list(policies) evaluations = [ self.build_evaluation_result(action_name, resource_arn, policy_statements) diff --git a/tests/aws/services/iam/test_iam.py b/tests/aws/services/iam/test_iam.py index 77ea19da586ee..e6315e6542606 100755 --- a/tests/aws/services/iam/test_iam.py +++ b/tests/aws/services/iam/test_iam.py @@ -437,40 +437,77 @@ def test_attach_detach_role_policy(self, aws_client, region_name): aws_client.iam.delete_policy(PolicyArn=policy_arn) - @markers.aws.needs_fixing - def test_simulate_principle_policy(self, aws_client): - # FIXME this test should test whether a principal (like user, role) has some permissions, it cannot test - # the policy itself - policy_name = "policy-{}".format(short_uid()) - policy_document = { - "Version": "2012-10-17", - "Statement": [ + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..EvaluationResults"]) + @pytest.mark.parametrize("arn_type", ["role", "group", "user"]) + def test_simulate_principle_policy( + self, + arn_type, + aws_client, + create_role, + create_policy, + create_user, + s3_bucket, + snapshot, + cleanups, + ): + bucket = s3_bucket + snapshot.add_transformer(snapshot.transform.regex(bucket, "bucket")) + snapshot.add_transformer(snapshot.transform.key_value("SourcePolicyId")) + + policy_arn = create_policy( + PolicyDocument=json.dumps( { - "Action": ["s3:GetObjectVersion", "s3:ListBucket"], - "Effect": "Allow", - "Resource": ["arn:aws:s3:::bucket_name"], + "Version": "2012-10-17", + "Statement": { + "Sid": "", + "Effect": "Allow", + "Action": "s3:PutObject", + "Resource": "*", + }, } - ], - } - - policy_arn = aws_client.iam.create_policy( - PolicyName=policy_name, Path="/", PolicyDocument=json.dumps(policy_document) + ) )["Policy"]["Arn"] + if arn_type == "role": + role_name = f"role-{short_uid()}" + role_arn = create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps( + { + "Version": "2012-10-17", + "Statement": { + "Sid": "", + "Effect": "Allow", + "Principal": {"Service": "apigateway.amazonaws.com"}, + "Action": "sts:AssumeRole", + }, + } + ), + )["Role"]["Arn"] + aws_client.iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn) + arn = role_arn + + elif arn_type == "group": + group_name = f"group-{short_uid()}" + group = aws_client.iam.create_group(GroupName=group_name)["Group"] + cleanups.append(lambda _: aws_client.iam.delete_group(GroupName=group_name)) + aws_client.iam.attach_group_policy(GroupName=group_name, PolicyArn=policy_arn) + arn = group["Arn"] + + else: + user_name = f"user-{short_uid()}" + user = create_user(UserName=user_name)["User"] + aws_client.iam.attach_user_policy(UserName=user_name, PolicyArn=policy_arn) + arn = user["Arn"] + rs = aws_client.iam.simulate_principal_policy( - PolicySourceArn=policy_arn, + PolicySourceArn=arn, ActionNames=["s3:PutObject", "s3:GetObjectVersion"], - ResourceArns=["arn:aws:s3:::bucket_name"], + ResourceArns=[f"arn:aws:s3:::{bucket}"], ) - assert rs["ResponseMetadata"]["HTTPStatusCode"] == 200 - evaluation_results = rs["EvaluationResults"] - assert len(evaluation_results) == 2 - - actions = {evaluation["EvalActionName"]: evaluation for evaluation in evaluation_results} - assert "s3:PutObject" in actions - assert actions["s3:PutObject"]["EvalDecision"] == "explicitDeny" - assert "s3:GetObjectVersion" in actions - assert actions["s3:GetObjectVersion"]["EvalDecision"] == "allowed" + + snapshot.match("response", rs) @markers.aws.validated def test_create_role_with_assume_role_policy(self, aws_client, account_id, create_role): diff --git a/tests/aws/services/iam/test_iam.snapshot.json b/tests/aws/services/iam/test_iam.snapshot.json index d33c8049e88c1..8fd4e2790a27d 100644 --- a/tests/aws/services/iam/test_iam.snapshot.json +++ b/tests/aws/services/iam/test_iam.snapshot.json @@ -6241,5 +6241,239 @@ } } } + }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[role]": { + "recorded-date": "21-04-2025, 20:07:35", + "recorded-content": { + "response": { + "EvaluationResults": [ + { + "EvalActionName": "s3:PutObject", + "EvalDecision": "allowed", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "allowed", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [] + } + ] + }, + { + "EvalActionName": "s3:GetObjectVersion", + "EvalDecision": "implicitDeny", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "implicitDeny", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [] + } + ] + } + ], + "IsTruncated": false, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[group]": { + "recorded-date": "21-04-2025, 20:07:37", + "recorded-content": { + "response": { + "EvaluationResults": [ + { + "EvalActionName": "s3:PutObject", + "EvalDecision": "allowed", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "allowed", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [] + } + ] + }, + { + "EvalActionName": "s3:GetObjectVersion", + "EvalDecision": "implicitDeny", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "implicitDeny", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [] + } + ] + } + ], + "IsTruncated": false, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[user]": { + "recorded-date": "21-04-2025, 20:07:38", + "recorded-content": { + "response": { + "EvaluationResults": [ + { + "EvalActionName": "s3:PutObject", + "EvalDecision": "allowed", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "allowed", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [ + { + "EndPosition": { + "Column": 113, + "Line": 1 + }, + "SourcePolicyId": "", + "SourcePolicyType": "IAM Policy", + "StartPosition": { + "Column": 41, + "Line": 1 + } + } + ], + "MissingContextValues": [] + } + ] + }, + { + "EvalActionName": "s3:GetObjectVersion", + "EvalDecision": "implicitDeny", + "EvalDecisionDetails": {}, + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [], + "OrganizationsDecisionDetail": { + "AllowedByOrganizations": true + }, + "ResourceSpecificResults": [ + { + "EvalResourceDecision": "implicitDeny", + "EvalResourceName": "arn::s3:::bucket", + "MatchedStatements": [], + "MissingContextValues": [] + } + ] + } + ], + "IsTruncated": false, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/iam/test_iam.validation.json b/tests/aws/services/iam/test_iam.validation.json index 9dad0bd733ca2..a1858c4acfeaf 100644 --- a/tests/aws/services/iam/test_iam.validation.json +++ b/tests/aws/services/iam/test_iam.validation.json @@ -47,6 +47,15 @@ "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_role_attach_policy": { "last_validated_date": "2025-03-06T12:25:03+00:00" }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[group]": { + "last_validated_date": "2025-04-21T20:07:37+00:00" + }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[role]": { + "last_validated_date": "2025-04-21T20:07:35+00:00" + }, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy[user]": { + "last_validated_date": "2025-04-21T20:07:38+00:00" + }, "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_update_assume_role_policy": { "last_validated_date": "2025-03-06T12:24:58+00:00" }, From 560b4cb9ce883e83c434e8780b5322d1ddcfca44 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:53:49 +0200 Subject: [PATCH 076/108] APIGW: fix Host regex to allow hyphen and remove restriction (#12549) --- .../apigateway/next_gen/execute_api/router.py | 2 +- .../services/apigateway/next_gen/provider.py | 14 -------- .../apigateway/test_apigateway_api.py | 17 ---------- .../apigateway/test_apigateway_common.py | 32 +++++++++++++++++++ 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/next_gen/execute_api/router.py b/localstack-core/localstack/services/apigateway/next_gen/execute_api/router.py index 7e84967df5004..93f509b8aed88 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/execute_api/router.py +++ b/localstack-core/localstack/services/apigateway/next_gen/execute_api/router.py @@ -124,7 +124,7 @@ def __init__(self, router: Router[Handler] = None, handler: ApiGatewayEndpoint = def register_routes(self) -> None: LOG.debug("Registering API Gateway routes.") - host_pattern = ".execute-api." + host_pattern = ".execute-api." deprecated_route_endpoint = deprecated_endpoint( endpoint=self.handler, previous_path="/restapis///_user_request_", diff --git a/localstack-core/localstack/services/apigateway/next_gen/provider.py b/localstack-core/localstack/services/apigateway/next_gen/provider.py index 43b05da4ddc3c..9c3dab33bfe86 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/provider.py +++ b/localstack-core/localstack/services/apigateway/next_gen/provider.py @@ -1,8 +1,6 @@ from localstack.aws.api import CommonServiceException, RequestContext, handler from localstack.aws.api.apigateway import ( - BadRequestException, CacheClusterSize, - CreateRestApiRequest, CreateStageRequest, Deployment, DeploymentCanarySettings, @@ -14,14 +12,12 @@ NotFoundException, NullableBoolean, NullableInteger, - RestApi, Stage, StatusCode, String, TestInvokeMethodRequest, TestInvokeMethodResponse, ) -from localstack.constants import TAG_KEY_CUSTOM_ID from localstack.services.apigateway.helpers import ( get_apigateway_store, get_moto_rest_api, @@ -60,16 +56,6 @@ def on_after_init(self): apply_patches() self.router.register_routes() - @handler("CreateRestApi", expand=False) - def create_rest_api(self, context: RequestContext, request: CreateRestApiRequest) -> RestApi: - if "-" in request.get("tags", {}).get(TAG_KEY_CUSTOM_ID, ""): - raise BadRequestException( - f"The '{TAG_KEY_CUSTOM_ID}' tag cannot contain the '-' character." - ) - - response = super().create_rest_api(context, request) - return response - @handler("DeleteRestApi") def delete_rest_api(self, context: RequestContext, rest_api_id: String, **kwargs) -> None: super().delete_rest_api(context, rest_api_id, **kwargs) diff --git a/tests/aws/services/apigateway/test_apigateway_api.py b/tests/aws/services/apigateway/test_apigateway_api.py index 612c956bbbd1c..2ae1dc9571811 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.py +++ b/tests/aws/services/apigateway/test_apigateway_api.py @@ -10,7 +10,6 @@ from localstack_snapshot.snapshots.transformer import KeyValueBasedTransformer, SortingTransformer from localstack.aws.api.apigateway import PutMode -from localstack.constants import TAG_KEY_CUSTOM_ID from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.files import load_file @@ -200,22 +199,6 @@ def test_create_rest_api_with_tags(self, apigw_create_rest_api, snapshot, aws_cl response = aws_client.apigateway.get_rest_apis() snapshot.match("get-rest-apis-w-tags", response) - @markers.aws.only_localstack - def test_create_rest_api_with_custom_id_tag(self, apigw_create_rest_api, aws_client): - custom_id_tag = "testid123" - response = apigw_create_rest_api( - name="my_api", description="this is my api", tags={TAG_KEY_CUSTOM_ID: custom_id_tag} - ) - api_id = response["id"] - assert api_id == custom_id_tag - - with pytest.raises(aws_client.apigateway.exceptions.BadRequestException): - apigw_create_rest_api( - name="my_api", - description="bad custom id", - tags={TAG_KEY_CUSTOM_ID: "bad-custom-id-hyphen"}, - ) - @markers.aws.validated def test_update_rest_api_operation_add_remove( self, apigw_create_rest_api, snapshot, aws_client diff --git a/tests/aws/services/apigateway/test_apigateway_common.py b/tests/aws/services/apigateway/test_apigateway_common.py index b0477593c8241..c585df9dcb05d 100644 --- a/tests/aws/services/apigateway/test_apigateway_common.py +++ b/tests/aws/services/apigateway/test_apigateway_common.py @@ -8,6 +8,7 @@ from botocore.exceptions import ClientError from localstack.aws.api.lambda_ import Runtime +from localstack.constants import TAG_KEY_CUSTOM_ID from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.aws.arns import get_partition, parse_arn @@ -1787,3 +1788,34 @@ def test_api_not_existing(self, aws_client, create_rest_apigw, snapshot): assert _response.json() == { "message": "The API id '404api' does not correspond to a deployed API Gateway API" } + + @markers.aws.only_localstack + def test_routing_with_custom_api_id(self, aws_client, create_rest_apigw): + custom_id = "custom-api-id" + api_id, _, root_id = create_rest_apigw( + name="test custom id routing", tags={TAG_KEY_CUSTOM_ID: custom_id} + ) + + resource = aws_client.apigateway.create_resource( + restApiId=api_id, parentId=root_id, pathPart="part1" + ) + hardcoded_resource_id = resource["id"] + + response_template_get = {"statusCode": 200, "message": "routing ok"} + _create_mock_integration_with_200_response_template( + aws_client, api_id, hardcoded_resource_id, "GET", response_template_get + ) + + stage_name = "dev" + aws_client.apigateway.create_deployment(restApiId=api_id, stageName=stage_name) + + url = api_invoke_url(api_id=api_id, stage=stage_name, path="/part1") + response = requests.get(url) + assert response.ok + assert response.json()["message"] == "routing ok" + + # Validated test living here: `test_create_execute_api_vpc_endpoint` + vpce_url = url.replace(custom_id, f"{custom_id}-vpce-aabbaabbaabbaabba") + response = requests.get(vpce_url) + assert response.ok + assert response.json()["message"] == "routing ok" From 49dcd934a8983e85f6870a254439884a17434923 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 23 Apr 2025 18:16:21 +0200 Subject: [PATCH 077/108] Clarify coverage error messages to distinguish license and emulation limits (#12547) --- .../localstack/utils/coverage_docs.py | 21 +++++++++---------- tests/unit/aws/test_skeleton.py | 14 +++++++++---- tests/unit/utils/test_coverage_docs.py | 14 +++++++------ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/localstack-core/localstack/utils/coverage_docs.py b/localstack-core/localstack/utils/coverage_docs.py index 43649df5fd102..fde4628a32f67 100644 --- a/localstack-core/localstack/utils/coverage_docs.py +++ b/localstack-core/localstack/utils/coverage_docs.py @@ -1,8 +1,4 @@ -COVERAGE_LINK_BASE = "https://docs.localstack.cloud/references/coverage/" -MESSAGE_TEMPLATE = ( - f"API %sfor service '%s' not yet implemented or pro feature" - f" - please check {COVERAGE_LINK_BASE}%s for further information" -) +_COVERAGE_LINK_BASE = "https://docs.localstack.cloud/references/coverage" def get_coverage_link_for_service(service_name: str, action_name: str) -> str: @@ -11,11 +7,14 @@ def get_coverage_link_for_service(service_name: str, action_name: str) -> str: available_services = SERVICE_PLUGINS.list_available() if service_name not in available_services: - return MESSAGE_TEMPLATE % ("", service_name, "") - + return ( + f"The API for service '{service_name}' is either not included in your current license plan " + "or has not yet been emulated by LocalStack. " + f"Please refer to {_COVERAGE_LINK_BASE} for more details." + ) else: - return MESSAGE_TEMPLATE % ( - f"action '{action_name}' ", - service_name, - f"coverage_{service_name}/", + return ( + f"The API action '{action_name}' for service '{service_name}' is either not available in " + "your current license plan or has not yet been emulated by LocalStack. " + f"Please refer to {_COVERAGE_LINK_BASE}/coverage_{service_name} for more information." ) diff --git a/tests/unit/aws/test_skeleton.py b/tests/unit/aws/test_skeleton.py index 9068dd8d77713..03fd831e04392 100644 --- a/tests/unit/aws/test_skeleton.py +++ b/tests/unit/aws/test_skeleton.py @@ -200,8 +200,11 @@ def test_skeleton_e2e_sqs_send_message(): [ ( TestSqsApiNotImplemented(), - "API action 'SendMessage' for service 'sqs' not yet implemented or pro feature" - " - please check https://docs.localstack.cloud/references/coverage/coverage_sqs/ for further information", + ( + "The API action 'SendMessage' for service 'sqs' is either not available " + "in your current license plan or has not yet been emulated by LocalStack. " + "Please refer to https://docs.localstack.cloud/references/coverage/coverage_sqs for more information." + ), ), ( TestSqsApiNotImplementedWithMessage(), @@ -312,8 +315,11 @@ def test_dispatch_missing_method_returns_internal_failure(): assert "Error" in parsed_response assert parsed_response["Error"] == { "Code": "InternalFailure", - "Message": "API action 'DeleteQueue' for service 'sqs' not yet implemented or pro feature - please check " - "https://docs.localstack.cloud/references/coverage/coverage_sqs/ for further information", + "Message": ( + "The API action 'DeleteQueue' for service 'sqs' is either not available in your " + "current license plan or has not yet been emulated by LocalStack. " + "Please refer to https://docs.localstack.cloud/references/coverage/coverage_sqs for more information." + ), } diff --git a/tests/unit/utils/test_coverage_docs.py b/tests/unit/utils/test_coverage_docs.py index ee9a0a88dccec..b21442736295a 100644 --- a/tests/unit/utils/test_coverage_docs.py +++ b/tests/unit/utils/test_coverage_docs.py @@ -3,15 +3,17 @@ def test_coverage_link_for_existing_service(): coverage_link = get_coverage_link_for_service("s3", "random_action") - assert ( - coverage_link - == "API action 'random_action' for service 's3' not yet implemented or pro feature - please check https://docs.localstack.cloud/references/coverage/coverage_s3/ for further information" + assert coverage_link == ( + "The API action 'random_action' for service 's3' is either not available in your current " + "license plan or has not yet been emulated by LocalStack. " + "Please refer to https://docs.localstack.cloud/references/coverage/coverage_s3 for more information." ) def test_coverage_link_for_non_existing_service(): coverage_link = get_coverage_link_for_service("dummy_service", "random_action") - assert ( - coverage_link - == "API for service 'dummy_service' not yet implemented or pro feature - please check https://docs.localstack.cloud/references/coverage/ for further information" + assert coverage_link == ( + "The API for service 'dummy_service' is either not included in your current license plan or " + "has not yet been emulated by LocalStack. " + "Please refer to https://docs.localstack.cloud/references/coverage for more details." ) From 9a401816062f24737f5d0cbd6e2a4a4d9ff57ef6 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:13:54 +0200 Subject: [PATCH 078/108] Step Functions: Decrease LocalStack Sampling Delays to Speed Up Tests Suite (#12550) --- .../testing/pytest/stepfunctions/utils.py | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py index bb55f99bc3958..cd55ef0106aa5 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py @@ -27,6 +27,7 @@ from localstack.services.stepfunctions.asl.eval.event.logging import is_logging_enabled_for from localstack.services.stepfunctions.asl.utils.encoding import to_json_str from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError, extract_json +from localstack.testing.aws.util import is_aws_cloud from localstack.utils.strings import short_uid from localstack.utils.sync import poll_condition @@ -36,6 +37,16 @@ # For EXPRESS state machines, the deletion will happen eventually (usually less than a minute). # Running executions may emit logs after DeleteStateMachine API is called. _DELETION_TIMEOUT_SECS: Final[int] = 120 +_SAMPLING_INTERVAL_SECONDS_AWS_CLOUD: Final[int] = 1 +_SAMPLING_INTERVAL_SECONDS_LOCALSTACK: Final[float] = 0.2 + + +def _get_sampling_interval_seconds() -> int | float: + return ( + _SAMPLING_INTERVAL_SECONDS_AWS_CLOUD + if is_aws_cloud() + else _SAMPLING_INTERVAL_SECONDS_LOCALSTACK + ) def await_no_state_machines_listed(stepfunctions_client): @@ -47,7 +58,7 @@ def _is_empty_state_machine_list(): success = poll_condition( condition=_is_empty_state_machine_list, timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning("Timed out whilst awaiting for listing to be empty.") @@ -76,7 +87,7 @@ def await_state_machine_alias_is_created( state_machine_alias_arn=state_machine_alias_arn, ), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning("Timed out whilst awaiting for listing to be empty.") @@ -92,7 +103,7 @@ def await_state_machine_alias_is_deleted( state_machine_alias_arn=state_machine_alias_arn, ), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning("Timed out whilst awaiting for listing to be empty.") @@ -122,7 +133,7 @@ def await_state_machine_not_listed(stepfunctions_client, state_machine_arn: str) success = poll_condition( condition=lambda: not _is_state_machine_listed(stepfunctions_client, state_machine_arn), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning("Timed out whilst awaiting for listing to exclude '%s'.", state_machine_arn) @@ -132,7 +143,7 @@ def await_state_machine_listed(stepfunctions_client, state_machine_arn: str): success = poll_condition( condition=lambda: _is_state_machine_listed(stepfunctions_client, state_machine_arn), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning("Timed out whilst awaiting for listing to include '%s'.", state_machine_arn) @@ -146,7 +157,7 @@ def await_state_machine_version_not_listed( stepfunctions_client, state_machine_arn, state_machine_version_arn ), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning( @@ -164,7 +175,7 @@ def await_state_machine_version_listed( stepfunctions_client, state_machine_arn, state_machine_version_arn ), timeout=_DELETION_TIMEOUT_SECS, - interval=1, + interval=_get_sampling_interval_seconds(), ) if not success: LOG.warning( @@ -190,7 +201,9 @@ def _run_check(): res: bool = check_func(events) return res - assert poll_condition(condition=_run_check, timeout=120, interval=1) + assert poll_condition( + condition=_run_check, timeout=120, interval=_get_sampling_interval_seconds() + ) return events @@ -223,7 +236,9 @@ def _run_check(): return True return False - success = poll_condition(condition=_run_check, timeout=120, interval=1) + success = poll_condition( + condition=_run_check, timeout=120, interval=_get_sampling_interval_seconds() + ) if not success: LOG.warning( "Timed out whilst awaiting for execution status %s to satisfy condition for execution '%s'.", @@ -264,7 +279,9 @@ def _check_last_is_terminal() -> bool: return execution["status"] != ExecutionStatus.RUNNING return False - success = poll_condition(condition=_check_last_is_terminal, timeout=120, interval=1) + success = poll_condition( + condition=_check_last_is_terminal, timeout=120, interval=_get_sampling_interval_seconds() + ) if not success: LOG.warning( "Timed out whilst awaiting for execution events to satisfy condition for execution '%s'.", @@ -291,7 +308,9 @@ def _run_check(): status: ExecutionStatus = desc_res["status"] return status == ExecutionStatus.ABORTED - success = poll_condition(condition=_run_check, timeout=120, interval=1) + success = poll_condition( + condition=_run_check, timeout=120, interval=_get_sampling_interval_seconds() + ) if not success: LOG.warning("Timed out whilst awaiting for execution '%s' to abort.", execution_arn) From 98c3e96a8bb3c7440c8fa1fd0d0ce282a5713e2a Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:45:25 +0200 Subject: [PATCH 079/108] Cloud Formation v2 Engine: Support for Default fields in Parameters (#12537) --- .../engine/v2/change_set_model.py | 32 +- .../engine/v2/change_set_model_preproc.py | 12 +- .../cloudformation/api/test_changesets.py | 4 +- .../v2/test_change_set_conditions.py | 5 +- .../v2/test_change_set_fn_get_attr.py | 5 +- .../v2/test_change_set_mappings.py | 5 +- .../v2/test_change_set_parameters.py | 131 ++ .../test_change_set_parameters.snapshot.json | 1930 +++++++++++++++++ ...test_change_set_parameters.validation.json | 14 + .../cloudformation/v2/test_change_set_ref.py | 5 +- 10 files changed, 2126 insertions(+), 17 deletions(-) create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_parameters.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_parameters.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_parameters.validation.json diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 93ef9b32aae41..f8adc872cbc2a 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -149,21 +149,24 @@ def __init__(self, scope: Scope, value: ChangeSetEntity, divergence: ChangeSetEn class NodeParameter(ChangeSetNode): name: Final[str] - value: Final[ChangeSetEntity] + type_: Final[ChangeSetEntity] dynamic_value: Final[ChangeSetEntity] + default_value: Final[Optional[ChangeSetEntity]] def __init__( self, scope: Scope, change_type: ChangeType, name: str, - value: ChangeSetEntity, + type_: ChangeSetEntity, dynamic_value: ChangeSetEntity, + default_value: Optional[ChangeSetEntity], ): super().__init__(scope=scope, change_type=change_type) self.name = name - self.value = value + self.type_ = type_ self.dynamic_value = dynamic_value + self.default_value = default_value class NodeParameters(ChangeSetNode): @@ -358,6 +361,7 @@ def __init__(self, scope: Scope, value: Any): ResourcesKey: Final[str] = "Resources" PropertiesKey: Final[str] = "Properties" ParametersKey: Final[str] = "Parameters" +DefaultKey: Final[str] = "Default" ValueKey: Final[str] = "Value" ExportKey: Final[str] = "Export" OutputsKey: Final[str] = "Outputs" @@ -855,19 +859,29 @@ def _visit_parameter( node_parameter = self._visited_scopes.get(scope) if isinstance(node_parameter, NodeParameter): return node_parameter - # TODO: add logic to compute defaults already in the graph building process? - dynamic_value = self._visit_dynamic_parameter(parameter_name=parameter_name) - value = self._visit_value( - scope=scope, before_value=before_parameter, after_value=after_parameter + + type_scope, (before_type, after_type) = self._safe_access_in( + scope, TypeKey, before_parameter, after_parameter ) + type_ = self._visit_value(type_scope, before_type, after_type) + + default_scope, (before_default, after_default) = self._safe_access_in( + scope, DefaultKey, before_parameter, after_parameter + ) + default_value = self._visit_value(default_scope, before_default, after_default) + + dynamic_value = self._visit_dynamic_parameter(parameter_name=parameter_name) + change_type = self._change_type_for_parent_of( - change_types=[dynamic_value.change_type, value.change_type] + change_types=[type_.change_type, default_value.change_type, dynamic_value.change_type] ) + node_parameter = NodeParameter( scope=scope, change_type=change_type, name=parameter_name, - value=value, + type_=type_, + default_value=default_value, dynamic_value=dynamic_value, ) self._visited_scopes[scope] = node_parameter diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index 6c24510f2bef4..08382da63faf2 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -407,10 +407,16 @@ def visit_node_mapping(self, node_mapping: NodeMapping) -> PreprocEntityDelta: return bindings_delta def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta: - # TODO: add support for default value sampling dynamic_value = node_parameter.dynamic_value - delta = self.visit(dynamic_value) - return delta + dynamic_delta = self.visit(dynamic_value) + + default_value = node_parameter.default_value + default_delta = self.visit(default_value) + + before = dynamic_delta.before or default_delta.before + after = dynamic_delta.after or default_delta.after + + return PreprocEntityDelta(before=before, after=after) def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDelta: delta = self.visit(node_condition.body) diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index 56af7886cb019..c00a21845388e 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -1211,7 +1211,9 @@ def test_describe_change_set_with_similarly_named_stacks(deploy_cfn_template, aw ) -@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) @markers.snapshot.skip_snapshot_verify( paths=[ "per-resource-events..*", diff --git a/tests/aws/services/cloudformation/v2/test_change_set_conditions.py b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py index 69312a633fd04..9967f6cf4b607 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_conditions.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_conditions.py @@ -2,11 +2,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import long_uid -@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) @markers.snapshot.skip_snapshot_verify( paths=[ "per-resource-events..*", diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py index 91fa1122aa4b3..01719bdea7778 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_get_attr.py @@ -2,11 +2,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import long_uid -@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) @markers.snapshot.skip_snapshot_verify( paths=[ "per-resource-events..*", diff --git a/tests/aws/services/cloudformation/v2/test_change_set_mappings.py b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py index d6d4573ac80ee..55c9d5d1b5197 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_mappings.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_mappings.py @@ -2,11 +2,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import long_uid -@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) @markers.snapshot.skip_snapshot_verify( paths=[ "per-resource-events..*", diff --git a/tests/aws/services/cloudformation/v2/test_change_set_parameters.py b/tests/aws/services/cloudformation/v2/test_change_set_parameters.py new file mode 100644 index 0000000000000..50c371dad8186 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_parameters.py @@ -0,0 +1,131 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + "$..ChangeSetId", # An issue for the WIP executor + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + ] +) +class TestChangeSetParameters: + @markers.aws.validated + def test_update_parameter_default_value( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Parameters": {"TopicName": {"Type": "String", "Default": name1}}, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": {"Ref": "TopicName"}}, + }, + }, + } + template_2 = { + "Parameters": {"TopicName": {"Type": "String", "Default": name2}}, + "Resources": template_1["Resources"], + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_update_parameter_with_added_default_value( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Parameters": {"TopicName": {"Type": "String"}}, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": {"Ref": "TopicName"}}, + }, + }, + } + template_2 = { + "Parameters": {"TopicName": {"Type": "String", "Default": name2}}, + "Resources": template_1["Resources"], + } + capture_update_process(snapshot, template_1, template_2, p1={"TopicName": name1}) + + @markers.aws.validated + def test_update_parameter_with_removed_default_value( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Parameters": {"TopicName": {"Type": "String", "Default": name1}}, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": {"Ref": "TopicName"}}, + }, + }, + } + template_2 = { + "Parameters": {"TopicName": {"Type": "String"}}, + "Resources": template_1["Resources"], + } + capture_update_process(snapshot, template_1, template_2, p2={"TopicName": name2}) + + @markers.aws.validated + def test_update_parameter_default_value_with_dynamic_overrides( + self, + snapshot, + capture_update_process, + ): + name1 = f"topic-name-1-{long_uid()}" + name2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + template_1 = { + "Parameters": {"TopicName": {"Type": "String", "Default": "default-value-1"}}, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": {"Ref": "TopicName"}}, + }, + }, + } + template_2 = { + "Parameters": {"TopicName": {"Type": "String", "Default": "default-value-2"}}, + "Resources": template_1["Resources"], + } + capture_update_process( + snapshot, template_1, template_2, p1={"TopicName": name1}, p2={"TopicName": name2} + ) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_parameters.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_parameters.snapshot.json new file mode 100644 index 0000000000000..4d0c44f81f248 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_parameters.snapshot.json @@ -0,0 +1,1930 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_default_value": { + "recorded-date": "17-04-2025, 15:35:43", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-6d79defd-40ea-4793-bbcc-fbcf6dcb6eb4", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-804ab46c-bf2c-477a-9da2-629781f29597", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_with_added_default_value": { + "recorded-date": "17-04-2025, 15:39:55", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-449f3796-5bc0-4441-a8e6-0b21e4a99416", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-a81a99de-0236-4beb-9be3-e32fa1cd7282", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_with_removed_default_value": { + "recorded-date": "17-04-2025, 15:44:25", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-26b9d263-5cf0-43f9-a362-8beefe1eccfb", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-b9d5ed41-3eba-434b-99f4-76d25a3a5252", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_default_value_with_dynamic_overrides": { + "recorded-date": "17-04-2025, 15:46:46", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-1c67b504-9b23-4cc3-8643-140d32564baa", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-13dc7d23-bc33-4e8f-a1bb-00c2675dbae1", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-name-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_parameters.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_parameters.validation.json new file mode 100644 index 0000000000000..05e1a75cbd323 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_parameters.validation.json @@ -0,0 +1,14 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_default_value": { + "last_validated_date": "2025-04-17T15:35:43+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_default_value_with_dynamic_overrides": { + "last_validated_date": "2025-04-17T15:46:46+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_with_added_default_value": { + "last_validated_date": "2025-04-17T15:39:55+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_parameters.py::TestChangeSetParameters::test_update_parameter_with_removed_default_value": { + "last_validated_date": "2025-04-17T15:44:24+00:00" + } +} diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.py b/tests/aws/services/cloudformation/v2/test_change_set_ref.py index 515cf3c967bd6..4ae58d9246c06 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_ref.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.py @@ -2,11 +2,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import long_uid -@pytest.mark.skipif(condition=not is_v2_engine(), reason="Requires the V2 engine") +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) @markers.snapshot.skip_snapshot_verify( paths=[ "per-resource-events..*", From e8d202986eeffc392f8fd1d93aa49f0123cfc621 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Thu, 24 Apr 2025 18:15:58 +0100 Subject: [PATCH 080/108] CFn v2: support outputs (#12536) --- .../engine/v2/change_set_model_executor.py | 30 +++++++++++++++++-- .../services/cloudformation/v2/entities.py | 18 ++++++++++- .../services/cloudformation/v2/provider.py | 7 +++-- .../cloudformation/api/test_changesets.py | 8 +++-- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index 4398338d9a9ae..4ce4c2fad2db1 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -1,17 +1,20 @@ import copy import logging import uuid +from dataclasses import dataclass from typing import Final, Optional from localstack.aws.api.cloudformation import ChangeAction, StackStatus from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY from localstack.services.cloudformation.engine.v2.change_set_model import ( + NodeOutput, NodeParameter, NodeResource, ) from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( ChangeSetModelPreproc, PreprocEntityDelta, + PreprocOutput, PreprocProperties, PreprocResource, ) @@ -27,10 +30,18 @@ LOG = logging.getLogger(__name__) +@dataclass +class ChangeSetModelExecutorResult: + resources: dict + parameters: dict + outputs: dict + + class ChangeSetModelExecutor(ChangeSetModelPreproc): _change_set: Final[ChangeSet] # TODO: add typing for resolved resources and parameters. resources: Final[dict] + outputs: Final[dict] resolved_parameters: Final[dict] def __init__(self, change_set: ChangeSet): @@ -40,12 +51,15 @@ def __init__(self, change_set: ChangeSet): ) self._change_set = change_set self.resources = dict() + self.outputs = dict() self.resolved_parameters = dict() # TODO: use a structured type for the return value - def execute(self) -> tuple[dict, dict]: + def execute(self) -> ChangeSetModelExecutorResult: self.process() - return self.resources, self.resolved_parameters + return ChangeSetModelExecutorResult( + resources=self.resources, parameters=self.resolved_parameters, outputs=self.outputs + ) def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta: delta = super().visit_node_parameter(node_parameter=node_parameter) @@ -82,6 +96,18 @@ def visit_node_resource( after_resource.physical_resource_id = after_physical_id return delta + def visit_node_output( + self, node_output: NodeOutput + ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]: + delta = super().visit_node_output(node_output=node_output) + if delta.after is None: + # handling deletion so the output does not really matter + # TODO: are there other situations? + return delta + + self.outputs[delta.after.name] = delta.after.value + return delta + def _execute_on_resource_change( self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource] ) -> None: diff --git a/localstack-core/localstack/services/cloudformation/v2/entities.py b/localstack-core/localstack/services/cloudformation/v2/entities.py index b73d33d917783..c3747dc44658b 100644 --- a/localstack-core/localstack/services/cloudformation/v2/entities.py +++ b/localstack-core/localstack/services/cloudformation/v2/entities.py @@ -8,6 +8,7 @@ CreateChangeSetInput, DescribeChangeSetOutput, ExecutionStatus, + Output, Parameter, StackDriftInformation, StackDriftStatus, @@ -48,6 +49,7 @@ class Stack: # state after deploy resolved_parameters: dict[str, str] resolved_resources: dict[str, ResolvedResource] + resolved_outputs: dict[str, str] def __init__( self, @@ -85,6 +87,7 @@ def __init__( # state after deploy self.resolved_parameters = {} self.resolved_resources = {} + self.resolved_outputs = {} def set_stack_status(self, status: StackStatus, reason: StackStatusReason | None = None): self.status = status @@ -92,7 +95,7 @@ def set_stack_status(self, status: StackStatus, reason: StackStatusReason | None self.status_reason = reason def describe_details(self) -> ApiStack: - return { + result = { "CreationTime": self.creation_time, "StackId": self.stack_id, "StackName": self.stack_name, @@ -108,6 +111,19 @@ def describe_details(self) -> ApiStack: "RollbackConfiguration": {}, "Tags": [], } + if self.resolved_outputs: + describe_outputs = [] + for key, value in self.resolved_outputs.items(): + describe_outputs.append( + Output( + # TODO(parity): Description, ExportName + # TODO(parity): what happens on describe stack when the stack has not been deployed yet? + OutputKey=key, + OutputValue=value, + ) + ) + result["Outputs"] = describe_outputs + return result class ChangeSet: diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index beec76b010390..48bff856d3c87 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -270,14 +270,15 @@ def execute_change_set( ) def _run(*args): - new_resources, new_parameters = change_set_executor.execute() + result = change_set_executor.execute() new_stack_status = StackStatus.UPDATE_COMPLETE if change_set.change_set_type == ChangeSetType.CREATE: new_stack_status = StackStatus.CREATE_COMPLETE change_set.stack.set_stack_status(new_stack_status) change_set.set_execution_status(ExecutionStatus.EXECUTE_COMPLETE) - change_set.stack.resolved_resources = new_resources - change_set.stack.resolved_parameters = new_parameters + change_set.stack.resolved_resources = result.resources + change_set.stack.resolved_parameters = result.parameters + change_set.stack.resolved_outputs = result.outputs start_worker_thread(_run) diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index c00a21845388e..61480d44f05f0 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -27,7 +27,6 @@ class TestUpdates: def test_simple_update_single_resource( self, aws_client: ServiceLevelClientFactory, deploy_cfn_template ): - parameter_name = "my-parameter" value1 = "foo" value2 = "bar" stack_name = f"stack-{short_uid()}" @@ -37,15 +36,20 @@ def test_simple_update_single_resource( "MyParameter": { "Type": "AWS::SSM::Parameter", "Properties": { - "Name": parameter_name, "Type": "String", "Value": value1, }, }, }, + "Outputs": { + "ParameterName": { + "Value": {"Ref": "MyParameter"}, + }, + }, } res = deploy_cfn_template(stack_name=stack_name, template=json.dumps(t1), is_update=False) + parameter_name = res.outputs["ParameterName"] found_value = aws_client.ssm.get_parameter(Name=parameter_name)["Parameter"]["Value"] assert found_value == value1 From 5deda39242c22eb419a62068a2991f0beca1c6aa Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Fri, 25 Apr 2025 12:50:19 +0200 Subject: [PATCH 081/108] Update lambda runtime init (#12555) --- localstack-core/localstack/services/lambda_/packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack-core/localstack/services/lambda_/packages.py b/localstack-core/localstack/services/lambda_/packages.py index fd893bc1591df..fd549c1c7ad34 100644 --- a/localstack-core/localstack/services/lambda_/packages.py +++ b/localstack-core/localstack/services/lambda_/packages.py @@ -13,7 +13,7 @@ """Customized LocalStack version of the AWS Lambda Runtime Interface Emulator (RIE). https://github.com/localstack/lambda-runtime-init/blob/localstack/README-LOCALSTACK.md """ -LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.32-pre" +LAMBDA_RUNTIME_DEFAULT_VERSION = "v0.1.33-pre" LAMBDA_RUNTIME_VERSION = config.LAMBDA_INIT_RELEASE_VERSION or LAMBDA_RUNTIME_DEFAULT_VERSION LAMBDA_RUNTIME_INIT_URL = "https://github.com/localstack/lambda-runtime-init/releases/download/{version}/aws-lambda-rie-{arch}" From b55fc1bd8ee6ff141a59d7eca144e7af898f0597 Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:37:39 +0200 Subject: [PATCH 082/108] Added new codeowners for CircleCI and GithubActions (#12558) --- CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9f806378e7f42..e165d6d3cc5d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,8 +17,8 @@ /Dockerfile @alexrashed # Git, Pipelines, GitHub config -/.circleci @alexrashed @dfangl @dominikschubert -/.github @alexrashed @dfangl @dominikschubert +/.circleci @alexrashed @dfangl @dominikschubert @silv-io @k-a-il +/.github @alexrashed @dfangl @dominikschubert @silv-io @k-a-il /.test_durations @alexrashed /.git-blame-ignore-revs @alexrashed @thrau /bin/release-dev.sh @thrau @alexrashed From 7a3666f903f9b3f735e82c477b388180a04c0588 Mon Sep 17 00:00:00 2001 From: Greg Furman <31275503+gregfurman@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:20:04 +0200 Subject: [PATCH 083/108] [ESM] Fix flaky discarding record age test (#12552) --- .../test_lambda_integration_kinesis.py | 64 +++++++++++++++---- ...lambda_integration_kinesis.validation.json | 2 +- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py index b2a864696bb5f..27906cb93f71d 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py @@ -1,3 +1,4 @@ +import base64 import json import math import time @@ -5,9 +6,12 @@ import pytest from botocore.exceptions import ClientError -from localstack_snapshot.snapshots.transformer import KeyValueBasedTransformer +from localstack_snapshot.snapshots.transformer import KeyValueBasedTransformer, SortingTransformer from localstack.aws.api.lambda_ import Runtime +from localstack.services.lambda_.event_source_mapping.pollers.kinesis_poller import ( + KinesisPoller, +) from localstack.testing.aws.lambda_utils import ( _await_event_source_mapping_enabled, _await_event_source_mapping_state, @@ -1071,7 +1075,7 @@ def _verify_messages_received(): # The record expired while retrying pytest.param(0, -1, id="expire-while-retrying"), # The record expired prior to arriving (no retries expected) - pytest.param(60, 0, id="expire-before-ingestion"), + pytest.param(60 if is_aws_cloud() else 5, 0, id="expire-before-ingestion"), ], ) def test_kinesis_maximum_record_age_exceeded( @@ -1084,7 +1088,6 @@ def test_kinesis_maximum_record_age_exceeded( wait_for_stream_ready, snapshot, aws_client, - region_name, sqs_create_queue, monkeypatch, # Parametrized arguments @@ -1107,15 +1110,27 @@ def test_kinesis_maximum_record_age_exceeded( "StreamDescription" ]["StreamARN"] + if not is_aws_cloud(): + # LocalStack test optimization: Override MaximumRecordAgeInSeconds directly + # in the poller to bypass the AWS API validation (where MaximumRecordAgeInSeconds >= 60s). + # This saves 55s waiting time. + def _patched_stream_parameters(self): + params = self.source_parameters.get("KinesisStreamParameters", {}) + params["MaximumRecordAgeInSeconds"] = 5 + return params + + monkeypatch.setattr( + KinesisPoller, "stream_parameters", property(_patched_stream_parameters) + ) + aws_client.kinesis.put_record( Data="stream-data", PartitionKey="test", StreamName=stream_name, ) - if processing_delay_seconds > 0: - # Optionally delay the ESM creation, allowing a record to expire prior to being ingested. - time.sleep(processing_delay_seconds) + # Optionally delay the ESM creation, allowing a record to expire prior to being ingested. + time.sleep(processing_delay_seconds) create_lambda_function( handler_file=TEST_LAMBDA_ECHO_FAILURE, @@ -1174,14 +1189,37 @@ def test_kinesis_maximum_record_age_exceeded_discard_records( snapshot, aws_client, sqs_create_queue, + monkeypatch, ): # snapshot setup snapshot.add_transformer(snapshot.transform.key_value("MD5OfBody")) snapshot.add_transformer(snapshot.transform.key_value("ReceiptHandle")) snapshot.add_transformer(snapshot.transform.key_value("startSequenceNumber")) + # PutRecords does not have guaranteed ordering so we should sort the retrieved records to ensure consistency + # between runs. + snapshot.add_transformer( + SortingTransformer( + "Records", lambda x: base64.b64decode(x["kinesis"]["data"]).decode("utf-8") + ), + ) + function_name = f"lambda_func-{short_uid()}" stream_name = f"test-kinesis-{short_uid()}" + wait_before_processing = 80 + + if not is_aws_cloud(): + wait_before_processing = 5 + + # LS test optimization + def _patched_stream_parameters(self): + params = self.source_parameters.get("KinesisStreamParameters", {}) + params["MaximumRecordAgeInSeconds"] = wait_before_processing + return params + + monkeypatch.setattr( + KinesisPoller, "stream_parameters", property(_patched_stream_parameters) + ) kinesis_create_stream(StreamName=stream_name, ShardCount=1) wait_for_stream_ready(stream_name=stream_name) @@ -1198,15 +1236,13 @@ def test_kinesis_maximum_record_age_exceeded_discard_records( ) # Ensure that the first record has expired - time.sleep(60) + time.sleep(wait_before_processing) - # The first record in the batch has expired with the remaining batch not exceeding any age-limits. - for i in range(5): - aws_client.kinesis.put_record( - Data=f"stream-data-{i + 1}", - PartitionKey="test", - StreamName=stream_name, - ) + # The first record in the batch will have expired with the remaining batch not exceeding any age-limits. + aws_client.kinesis.put_records( + Records=[{"Data": f"stream-data-{i + 1}", "PartitionKey": "test"} for i in range(5)], + StreamName=stream_name, + ) destination_queue_url = sqs_create_queue() create_lambda_function( diff --git a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json index 02855382cbf8e..4f3d4284e0547 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.validation.json @@ -42,7 +42,7 @@ "last_validated_date": "2025-04-13T16:39:43+00:00" }, "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_maximum_record_age_exceeded_discard_records": { - "last_validated_date": "2025-04-13T17:05:13+00:00" + "last_validated_date": "2025-04-23T21:42:09+00:00" }, "tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_report_batch_item_failure_scenarios[empty_string_item_identifier_failure]": { "last_validated_date": "2024-12-13T14:23:18+00:00" From ef035ab7a48b06cdc3fe58c1ff66b62fca4857e0 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 28 Apr 2025 15:37:05 +0200 Subject: [PATCH 084/108] Improve stream poller exception logging (#12520) --- .../event_source_mapping/pollers/stream_poller.py | 15 ++++++++++++--- .../lambda_/invocation/version_manager.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py index 5ebb51a2f7709..72d7c3ef3523b 100644 --- a/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +++ b/localstack-core/localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py @@ -311,9 +311,8 @@ def forward_events_to_target(self, shard_id, next_shard_iterator, records): # Discard all successful events and re-process from sequence number of failed event _, events = self.bisect_events(lowest_sequence_id, events) - except (BatchFailureError, Exception) as ex: - if isinstance(ex, BatchFailureError): - error_payload = ex.error + except BatchFailureError as ex: + error_payload = ex.error # FIXME partner_resource_arn is not defined in ESM LOG.debug( @@ -321,6 +320,16 @@ def forward_events_to_target(self, shard_id, next_shard_iterator, records): attempts, self.partner_resource_arn or self.source_arn, events, + exc_info=LOG.isEnabledFor(logging.DEBUG), + ) + except Exception: + # FIXME partner_resource_arn is not defined in ESM + LOG.error( + "Attempt %d failed with unexpected error while processing %s with events: %s", + attempts, + self.partner_resource_arn or self.source_arn, + events, + exc_info=LOG.isEnabledFor(logging.DEBUG), ) finally: # Retry polling until the record expires at the source diff --git a/localstack-core/localstack/services/lambda_/invocation/version_manager.py b/localstack-core/localstack/services/lambda_/invocation/version_manager.py index f8452816e1a7c..e53049dc82754 100644 --- a/localstack-core/localstack/services/lambda_/invocation/version_manager.py +++ b/localstack-core/localstack/services/lambda_/invocation/version_manager.py @@ -109,7 +109,7 @@ def start(self) -> VersionState: self.function_arn, self.function_version.config.internal_revision, e, - exc_info=True, + exc_info=LOG.isEnabledFor(logging.DEBUG), ) return self.state From 4e938e57d66a8bed723d79fd3dcfa55fefceab68 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 28 Apr 2025 15:48:46 +0200 Subject: [PATCH 085/108] feat: add current region and account as input to _proxy_capture_input_event (#12554) --- localstack-core/localstack/services/events/provider.py | 8 +++++--- localstack-core/localstack/services/events/utils.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index a26f5c63126ed..67e5e1bd9763e 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -1849,7 +1849,7 @@ def _process_entry( trace_header = context.trace_context["aws_trace_header"] - self._proxy_capture_input_event(event_formatted, trace_header) + self._proxy_capture_input_event(event_formatted, trace_header, region, account_id) # Always add the successful EventId entry, even if target processing might fail processed_entries.append({"EventId": event_formatted["id"]}) @@ -1867,8 +1867,10 @@ def _process_entry( ) ) - def _proxy_capture_input_event(self, event: FormattedEvent, trace_header: TraceHeader) -> None: - # only required for eventstudio to capture input event if no rule is configured + def _proxy_capture_input_event( + self, event: FormattedEvent, trace_header: TraceHeader, region: str, account_id: str + ) -> None: + # only required for EventStudio to capture input event if no rule is configured pass def _process_rules( diff --git a/localstack-core/localstack/services/events/utils.py b/localstack-core/localstack/services/events/utils.py index 36258ac668acb..5ac8e835b136f 100644 --- a/localstack-core/localstack/services/events/utils.py +++ b/localstack-core/localstack/services/events/utils.py @@ -187,6 +187,7 @@ def format_event( event: PutEventsRequestEntry, region: str, account_id: str, event_bus_name: EventBusName ) -> FormattedEvent: # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html + # region_name and account_id of original event is preserved fro cross-region event bus communication trace_header = event.get("TraceHeader") message = {} if trace_header: From e8b8794e37a025948b42f3b3f8aadd4958b7c97b Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:37:48 +0200 Subject: [PATCH 086/108] Step Functions: Improve Mocked Response Integration (#12553) --- .../state_task/lambda_eval_utils.py | 48 +- .../service/state_task_service_callback.py | 6 + .../service/state_task_service_lambda.py | 2 +- .../state_task/state_task_lambda.py | 2 +- .../stepfunctions/mocking/mock_config.py | 4 +- .../mocked_response_loader.py | 24 + .../dynamodb/200_get_item.json5 | 14 + .../dynamodb/200_put_item.json5 | 5 + .../mocked_responses/dynamodb/__init__.py | 0 .../events/200_put_events.json5 | 12 + .../mocked_responses/events/__init__.py | 0 .../not_ready_timeout_200_string_body.json5 | 22 + .../mocked_responses/sns/200_publish.json5 | 7 + .../mocked_responses/sns/__init__.py | 0 .../sqs/200_send_message.json5 | 8 + .../mocked_responses/sqs/__init__.py | 0 .../states/200_start_execution_sync.json5 | 20 + .../states/200_start_execution_sync2.json5 | 22 + .../mocked_responses/states/__init__.py | 0 .../v2/mocking/test_base_callbacks.py | 149 ++ .../mocking/test_base_callbacks.snapshot.json | 426 +++++ .../test_base_callbacks.validation.json | 8 + .../v2/mocking/test_base_scenarios.py | 483 ++++- .../mocking/test_base_scenarios.snapshot.json | 1628 +++++++++++++++++ .../test_base_scenarios.validation.json | 18 + 25 files changed, 2887 insertions(+), 21 deletions(-) create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/__init__.py create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/__init__.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py index 94cc1fc35817d..9f59414b844ab 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py @@ -6,9 +6,13 @@ from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.credentials import ( StateCredentials, ) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.mock_eval_utils import ( + eval_mocked_response, +) from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.utils.boto_client import boto_client_for from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.services.stepfunctions.mocking.mock_config import MockedResponse from localstack.utils.collections import select_from_typed_dict from localstack.utils.strings import to_bytes @@ -38,26 +42,46 @@ def _from_payload(payload_streaming_body: IO[bytes]) -> Union[json, str]: return decoded_data -def exec_lambda_function( - env: Environment, parameters: dict, region: str, state_credentials: StateCredentials -) -> None: +def _mocked_invoke_lambda_function(env: Environment) -> InvocationResponse: + mocked_response: MockedResponse = env.get_current_mocked_response() + eval_mocked_response(env=env, mocked_response=mocked_response) + invocation_resp: InvocationResponse = env.stack.pop() + return invocation_resp + + +def _invoke_lambda_function( + parameters: dict, region: str, state_credentials: StateCredentials +) -> InvocationResponse: lambda_client = boto_client_for( service="lambda", region=region, state_credentials=state_credentials ) - invocation_resp: InvocationResponse = lambda_client.invoke(**parameters) - - func_error: Optional[str] = invocation_resp.get("FunctionError") + invocation_response: InvocationResponse = lambda_client.invoke(**parameters) - payload = invocation_resp["Payload"] + payload = invocation_response["Payload"] payload_json = _from_payload(payload) - if func_error: - payload_str = json.dumps(payload_json, separators=(",", ":")) - raise LambdaFunctionErrorException(func_error, payload_str) + invocation_response["Payload"] = payload_json - invocation_resp["Payload"] = payload_json + return invocation_response + + +def execute_lambda_function_integration( + env: Environment, parameters: dict, region: str, state_credentials: StateCredentials +) -> None: + if env.is_mocked_mode(): + invocation_response: InvocationResponse = _mocked_invoke_lambda_function(env=env) + else: + invocation_response: InvocationResponse = _invoke_lambda_function( + parameters=parameters, region=region, state_credentials=state_credentials + ) + + function_error: Optional[str] = invocation_response.get("FunctionError") + if function_error: + payload_json = invocation_response["Payload"] + payload_str = json.dumps(payload_json, separators=(",", ":")) + raise LambdaFunctionErrorException(function_error, payload_str) - response = select_from_typed_dict(typed_dict=InvocationResponse, obj=invocation_resp) + response = select_from_typed_dict(typed_dict=InvocationResponse, obj=invocation_response) # noqa env.stack.append(response) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index 31c0e97dd9af5..8b9f56720fa08 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -176,12 +176,18 @@ def _eval_integration_pattern( outcome: CallbackOutcome | Any try: if self.resource.condition == ResourceCondition.WaitForTaskToken: + # WaitForTaskToken workflows are evaluated the same way, + # whether running in mock mode or not. outcome = self._eval_wait_for_task_token( env=env, timeout_seconds=timeout_seconds, callback_endpoint=callback_endpoint, heartbeat_endpoint=heartbeat_endpoint, ) + elif env.is_mocked_mode(): + # Sync operation in mock mode: sync and sync2 workflows are skipped and the outcome + # treated as the overall operation output. + outcome = task_output else: # Sync operations require the task output as input. env.stack.append(task_output) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py index 405dcf595d799..8feebfa1cdc29 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py @@ -124,7 +124,7 @@ def _eval_service_task( normalised_parameters: dict, state_credentials: StateCredentials, ): - lambda_eval_utils.exec_lambda_function( + lambda_eval_utils.execute_lambda_function_integration( env=env, parameters=normalised_parameters, region=resource_runtime_part.region, diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task_lambda.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task_lambda.py index a6a9dbe0c78d3..d33fc290b611e 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task_lambda.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task_lambda.py @@ -164,7 +164,7 @@ def _eval_execution(self, env: Environment) -> None: resource_runtime_part: ResourceRuntimePart = env.stack.pop() parameters["Payload"] = lambda_eval_utils.to_payload_type(parameters["Payload"]) - lambda_eval_utils.exec_lambda_function( + lambda_eval_utils.execute_lambda_function_integration( env=env, parameters=parameters, region=resource_runtime_part.region, diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py index f69b27eba6c55..3c3c64e03300f 100644 --- a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py @@ -57,10 +57,10 @@ def __init__( self.state_name = state_name self.mocked_response_name = mocked_response_name self.mocked_responses = list() - last_range_end: int = 0 + last_range_end: int = -1 mocked_responses_sorted = sorted(mocked_responses, key=lambda mr: mr.range_start) for mocked_response in mocked_responses_sorted: - if not mocked_response.range_start - last_range_end == 0: + if not mocked_response.range_start - last_range_end == 1: raise RuntimeError( f"Inconsistent event numbering detected for state '{state_name}': " f"the previous mocked response ended at event '{last_range_end}' " diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py index 032b72f5361b7..193e2d7d44a5f 100644 --- a/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py @@ -13,6 +13,30 @@ class MockedResponseLoader(abc.ABC): LAMBDA_200_STRING_BODY: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/lambda/200_string_body.json5" ) + LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/lambda/not_ready_timeout_200_string_body.json5" + ) + SQS_200_SEND_MESSAGE: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/sqs/200_send_message.json5" + ) + SNS_200_PUBLISH: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/sns/200_publish.json5" + ) + EVENTS_200_PUT_EVENTS: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/events/200_put_events.json5" + ) + DYNAMODB_200_PUT_ITEM: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/dynamodb/200_put_item.json5" + ) + DYNAMODB_200_GET_ITEM: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/dynamodb/200_get_item.json5" + ) + STATES_200_START_EXECUTION_SYNC: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/states/200_start_execution_sync.json5" + ) + STATES_200_START_EXECUTION_SYNC2: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/states/200_start_execution_sync2.json5" + ) @staticmethod def load(file_path: str) -> dict: diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 new file mode 100644 index 0000000000000..c4839f6a89a9a --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 @@ -0,0 +1,14 @@ +{ + "0": { + "Return": { + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + }, + } + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 new file mode 100644 index 0000000000000..7bd24326ab518 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 @@ -0,0 +1,5 @@ +{ + "0": { + "Return": {} + } +} diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 new file mode 100644 index 0000000000000..84ec05aff04e7 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 @@ -0,0 +1,12 @@ +{ + "0": { + "Return": { + "FailedEntryCount": 0, + "Entries": [ + { + "EventId": "11111111-2222-3333-4444-555555555555" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 new file mode 100644 index 0000000000000..d704190e3005e --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 @@ -0,0 +1,22 @@ +{ + "0": { + "Throw": { + "Error": "Lambda.ResourceNotReadyException", + "Cause": "This is a mocked lambda error" + } + }, + "1": { + "Throw": { + "Error": "Lambda.TimeoutException", + "Cause": "This is a mocked lambda error" + } + }, + "2": { + "Return": { + "Payload": { + "StatusCode": 200, + "body": "Hello from Lambda!" + } + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 new file mode 100644 index 0000000000000..e341f70ec719b --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 @@ -0,0 +1,7 @@ +{ + "0": { + "Return": { + "MessageId": "11112222-3333-4444-5555-666677778888" + } + } +} diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 new file mode 100644 index 0000000000000..e5dbcb270ca72 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 @@ -0,0 +1,8 @@ +{ + "0": { + "Return": { + "MD5OfMessageBody": "3bcb6e8e-7h85-4375-b0bc-1a59812c6e51", + "MessageId": "11112222-3333-4444-5555-666677778888", + } + } +} diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 new file mode 100644 index 0000000000000..da21dc9866b75 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 @@ -0,0 +1,20 @@ +{ + "0": { + "Return": { + "ExecutionArn": "arn:aws:states:us-east-1:111111111111:execution:Part:TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": '{"Arg1":"argument1"}', + "OutputDetails": { + "Included": true + }, + "StateMachineArn": "arn:aws:states:us-east-1:111111111111:stateMachine:Part", + "StartDate": "1745486528077", + "StopDate": "1745486528078", + "Status": "SUCCEEDED" + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 new file mode 100644 index 0000000000000..c957f70edd2bc --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 @@ -0,0 +1,22 @@ +{ + "0": { + "Return": { + "ExecutionArn": "arn:aws:states:us-east-1:111111111111:execution:Part:TestStartTarget", + "Input": {}, + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": { + "Arg1": "argument1" + }, + "OutputDetails": { + "Included": true + }, + "StateMachineArn": "arn:aws:states:us-east-1:111111111111:stateMachine:Part", + "StartDate": "1745486528077", + "StopDate": "1745486528078", + "Status": "SUCCEEDED" + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/__init__.py b/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py new file mode 100644 index 0000000000000..de007aea22301 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py @@ -0,0 +1,149 @@ +import json + +import pytest +from localstack_snapshot.snapshots.transformer import JsonpathTransformer + +from localstack import config +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, + create_and_record_mocked_execution, + create_state_machine_with_iam_role, +) +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( + MockedResponseLoader, +) +from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate +from tests.aws.services.stepfunctions.templates.callbacks.callback_templates import ( + CallbackTemplates, +) + + +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..SdkHttpMetadata", + "$..SdkResponseMetadata", + "$..ExecutedVersion", + "$..RedriveCount", + "$..RedriveStatus", + "$..RedriveStatusReason", + # In an effort to comply with SFN Local's lack of handling of sync operations, + # we are unable to produce valid TaskSubmittedEventDetails output field, which + # must include the provided mocked response in the output: + "$..events..taskSubmittedEventDetails.output", + ] +) +class TestBaseScenarios: + @markers.aws.validated + @pytest.mark.parametrize( + "template_file_path, mocked_response_filepath", + [ + ( + CallbackTemplates.SFN_START_EXECUTION_SYNC, + MockedResponseLoader.STATES_200_START_EXECUTION_SYNC, + ), + ( + CallbackTemplates.SFN_START_EXECUTION_SYNC2, + MockedResponseLoader.STATES_200_START_EXECUTION_SYNC2, + ), + ], + ids=["SFN_SYNC", "SFN_SYNC2"], + ) + def test_sfn_start_execution_sync( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + monkeypatch, + mock_config_file, + sfn_snapshot, + template_file_path, + mocked_response_filepath, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StopDate", + replacement="stop-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..StateMachineArn", + replacement="state-machine-arn", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..ExecutionArn", + replacement="execution-arn", + replace_reference=False, + ) + ) + + template = CallbackTemplates.load_sfn_template(template_file_path) + definition = json.dumps(template) + + if is_aws_cloud(): + template_target = BaseTemplate.load_sfn_template(BaseTemplate.BASE_PASS_RESULT) + definition_target = json.dumps(template_target) + state_machine_arn_target = create_state_machine_with_iam_role( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + exec_input = json.dumps( + { + "StateMachineArn": state_machine_arn_target, + "Input": None, + "Name": "TestStartTarget", + } + ) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + mocked_response = MockedResponseLoader.load(mocked_response_filepath) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"StartExecution": "mocked_response"}} + } + }, + "MockedResponses": {"mocked_response": mocked_response}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + exec_input = json.dumps( + {"StateMachineArn": "state-machine-arn", "Input": None, "Name": "TestStartTarget"} + ) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json new file mode 100644 index 0000000000000..39329e30b6dde --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json @@ -0,0 +1,426 @@ +{ + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sfn_start_execution_sync[SFN_SYNC]": { + "recorded-date": "24-04-2025, 10:05:48", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "state-machine-arn", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "state-machine-arn", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "state-machine-arn", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "164" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "164", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "StartExecution", + "output": { + "ExecutionArn": "execution-arn", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sfn_start_execution_sync[SFN_SYNC2]": { + "recorded-date": "24-04-2025, 10:06:22", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "state-machine-arn", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "state-machine-arn", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "state-machine-arn", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution.sync:2", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution.sync:2", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "164" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "164", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync:2", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "Input": {}, + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": { + "Arg1": "argument1" + }, + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync:2", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "StartExecution", + "output": { + "ExecutionArn": "execution-arn", + "Input": {}, + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": { + "Arg1": "argument1" + }, + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutionArn": "execution-arn", + "Input": {}, + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": { + "Arg1": "argument1" + }, + "OutputDetails": { + "Included": true + }, + "RedriveCount": 0, + "RedriveStatus": "NOT_REDRIVABLE", + "RedriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "StartDate": "start-date", + "StateMachineArn": "state-machine-arn", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json new file mode 100644 index 0000000000000..5bfb11ec0f06a --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json @@ -0,0 +1,8 @@ +{ + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sfn_start_execution_sync[SFN_SYNC2]": { + "last_validated_date": "2025-04-24T10:06:22+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sfn_start_execution_sync[SFN_SYNC]": { + "last_validated_date": "2025-04-24T10:05:48+00:00" + } +} diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py index 555047aa59843..e7a4736d79590 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py @@ -1,12 +1,14 @@ import json -from localstack_snapshot.snapshots.transformer import RegexTransformer +from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack import config from localstack.aws.api.lambda_ import Runtime +from localstack.aws.api.stepfunctions import HistoryEventType from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.testing.pytest.stepfunctions.utils import ( + await_execution_terminated, create_and_record_execution, create_and_record_mocked_execution, ) @@ -14,15 +16,159 @@ from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( MockedResponseLoader, ) -from tests.aws.services.stepfunctions.templates.services.services_templates import ( - ServicesTemplates as ST, +from tests.aws.services.stepfunctions.templates.scenarios.scenarios_templates import ( + ScenariosTemplate, ) +from tests.aws.services.stepfunctions.templates.services.services_templates import ServicesTemplates @markers.snapshot.skip_snapshot_verify( paths=["$..SdkHttpMetadata", "$..SdkResponseMetadata", "$..ExecutedVersion"] ) class TestBaseScenarios: + @markers.aws.validated + def test_lambda_invoke( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + create_lambda_function, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + function_name = f"lambda_{short_uid()}" + sfn_snapshot.add_transformer(RegexTransformer(function_name, "lambda_function_name")) + + template = ServicesTemplates.load_sfn_template(ServicesTemplates.LAMBDA_INVOKE_RESOURCE) + exec_input = json.dumps({"body": "string body"}) + + if is_aws_cloud(): + lambda_creation_response = create_lambda_function( + func_name=function_name, + handler_file=ServicesTemplates.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + lambda_arn = lambda_creation_response["CreateFunctionResponse"]["FunctionArn"] + template["States"]["step1"]["Resource"] = lambda_arn + definition = json.dumps(template) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + lambda_200_string_body = MockedResponseLoader.load( + MockedResponseLoader.LAMBDA_200_STRING_BODY + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"step1": "lambda_200_string_body"}} + } + }, + "MockedResponses": {"lambda_200_string_body": lambda_200_string_body}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + template["States"]["step1"]["Resource"] = ( + f"arn:aws:lambda:us-east-1:111111111111:function:{function_name}" + ) + definition = json.dumps(template) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.only_localstack + def test_lambda_invoke_retries( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + monkeypatch, + mock_config_file, + ): + template = ScenariosTemplate.load_sfn_template( + ScenariosTemplate.LAMBDA_INVOKE_WITH_RETRY_BASE + ) + template["States"]["InvokeLambdaWithRetry"]["Resource"] = ( + "arn:aws:lambda:us-east-1:111111111111:function:nosuchfunction" + ) + definition = json.dumps(template) + + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + lambda_not_ready_timeout_200_string_body = MockedResponseLoader.load( + MockedResponseLoader.LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": { + test_name: { + "InvokeLambdaWithRetry": "lambda_not_ready_timeout_200_string_body" + } + } + } + }, + "MockedResponses": { + "lambda_not_ready_timeout_200_string_body": lambda_not_ready_timeout_200_string_body + }, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + + role_arn = create_state_machine_iam_role(target_aws_client=aws_client) + + state_machine = create_state_machine( + target_aws_client=aws_client, + name=state_machine_name, + definition=definition, + roleArn=role_arn, + ) + state_machine_arn = state_machine["stateMachineArn"] + + sfn_client = aws_client.stepfunctions + execution = sfn_client.start_execution( + stateMachineArn=f"{state_machine_arn}#{test_name}", input="{}" + ) + execution_arn = execution["executionArn"] + + await_execution_terminated(stepfunctions_client=sfn_client, execution_arn=execution_arn) + + execution_history = sfn_client.get_execution_history( + executionArn=execution_arn, includeExecutionData=True + ) + events = execution_history["events"] + + event_4 = events[4] + assert event_4["taskFailedEventDetails"] == { + "error": "Lambda.ResourceNotReadyException", + "cause": "This is a mocked lambda error", + } + + event_7 = events[7] + assert event_7["taskFailedEventDetails"] == { + "error": "Lambda.TimeoutException", + "cause": "This is a mocked lambda error", + } + + last_event = events[-1] + assert last_event["type"] == HistoryEventType.ExecutionSucceeded + assert last_event["executionSucceededEventDetails"]["output"] == '{"Retries":2}' + @markers.aws.validated def test_lambda_service_invoke( self, @@ -34,7 +180,7 @@ def test_lambda_service_invoke( monkeypatch, mock_config_file, ): - template = ST.load_sfn_template(ST.LAMBDA_INVOKE) + template = ServicesTemplates.load_sfn_template(ServicesTemplates.LAMBDA_INVOKE) definition = json.dumps(template) function_name = f"lambda_{short_uid()}" @@ -44,7 +190,7 @@ def test_lambda_service_invoke( if is_aws_cloud(): create_lambda_function( func_name=function_name, - handler_file=ST.LAMBDA_ID_FUNCTION, + handler_file=ServicesTemplates.LAMBDA_ID_FUNCTION, runtime=Runtime.python3_12, ) create_and_record_execution( @@ -81,3 +227,330 @@ def test_lambda_service_invoke( state_machine_name, test_name, ) + + @markers.aws.validated + def test_sqs_send_message( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + create_lambda_function, + sqs_create_queue, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + + template = ServicesTemplates.load_sfn_template(ServicesTemplates.SQS_SEND_MESSAGE) + definition = json.dumps(template) + message_body = "test_message_body" + + if is_aws_cloud(): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs-queue-name")) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs-queue-url")) + + exec_input = json.dumps({"QueueUrl": queue_url, "MessageBody": message_body}) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + sqs_200_send_message = MockedResponseLoader.load( + MockedResponseLoader.SQS_200_SEND_MESSAGE + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"SendSQS": "sqs_200_send_message"}} + } + }, + "MockedResponses": {"sqs_200_send_message": sqs_200_send_message}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + exec_input = json.dumps({"QueueUrl": "sqs-queue-url", "MessageBody": message_body}) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.validated + def test_sns_publish_base( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + sns_create_topic, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + template = ServicesTemplates.load_sfn_template(ServicesTemplates.SNS_PUBLISH) + definition = json.dumps(template) + message_body = {"message": "string-literal"} + + if is_aws_cloud(): + topic = sns_create_topic() + topic_arn = topic["TopicArn"] + sfn_snapshot.add_transformer(RegexTransformer(topic_arn, "topic-arn")) + exec_input = json.dumps({"TopicArn": topic_arn, "Message": message_body}) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + sns_200_publish = MockedResponseLoader.load(MockedResponseLoader.SNS_200_PUBLISH) + mock_config = { + "StateMachines": { + state_machine_name: {"TestCases": {test_name: {"Publish": "sns_200_publish"}}} + }, + "MockedResponses": {"sns_200_publish": sns_200_publish}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + exec_input = json.dumps({"TopicArn": "topic-arn", "Message": message_body}) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.validated + def test_events_put_events( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + events_to_sqs_queue, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + detail_type = f"detail_type_{short_uid()}" + sfn_snapshot.add_transformer(RegexTransformer(detail_type, "detail-type")) + entries = [ + { + "Detail": json.dumps({"Message": "string-literal"}), + "DetailType": detail_type, + "Source": "some.source", + } + ] + + template = ServicesTemplates.load_sfn_template(ServicesTemplates.EVENTS_PUT_EVENTS) + definition = json.dumps(template) + + exec_input = json.dumps({"Entries": entries}) + + if is_aws_cloud(): + event_pattern = {"detail-type": [detail_type]} + queue_url = events_to_sqs_queue(event_pattern) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + events_200_put_events = MockedResponseLoader.load( + MockedResponseLoader.EVENTS_200_PUT_EVENTS + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"PutEvents": "events_200_put_events"}} + } + }, + "MockedResponses": {"events_200_put_events": events_200_put_events}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.validated + def test_dynamodb_put_get_item( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + events_to_sqs_queue, + dynamodb_create_table, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + template = ServicesTemplates.load_sfn_template(ServicesTemplates.DYNAMODB_PUT_GET_ITEM) + definition = json.dumps(template) + + table_name = f"sfn_test_table_{short_uid()}" + sfn_snapshot.add_transformer(RegexTransformer(table_name, "table-name")) + exec_input = json.dumps( + { + "TableName": table_name, + "Item": {"data": {"S": "string-literal"}, "id": {"S": "id1"}}, + "Key": {"id": {"S": "id1"}}, + } + ) + + if is_aws_cloud(): + dynamodb_create_table( + table_name=table_name, partition_key="id", client=aws_client.dynamodb + ) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + dynamodb_200_put_item = MockedResponseLoader.load( + MockedResponseLoader.DYNAMODB_200_PUT_ITEM + ) + dynamodb_200_get_item = MockedResponseLoader.load( + MockedResponseLoader.DYNAMODB_200_GET_ITEM + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": { + test_name: { + "PutItem": "dynamodb_200_put_item", + "GetItem": "dynamodb_200_get_item", + } + } + } + }, + "MockedResponses": { + "dynamodb_200_put_item": dynamodb_200_put_item, + "dynamodb_200_get_item": dynamodb_200_get_item, + }, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..events..previousEventId"]) + def test_map_state_lambda( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + create_lambda_function, + mock_config_file, + monkeypatch, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..mapRunArn", replacement="map_run_arn", replace_reference=False + ) + ) + + template = ScenariosTemplate.load_sfn_template( + ScenariosTemplate.MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT_LAMBDA + ) + # Update the lambda function's return value. + template["States"]["StartState"]["Parameters"]["Values"][0] = {"body": "string body"} + definition = json.dumps(template) + + exec_input = json.dumps({}) + if is_aws_cloud(): + function_name = f"sfn_lambda_{short_uid()}" + create_res = create_lambda_function( + func_name=function_name, + handler_file=ServicesTemplates.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + sfn_snapshot.add_transformer(RegexTransformer(function_name, "lambda_function_name")) + function_arn = create_res["CreateFunctionResponse"]["FunctionArn"] + definition = definition.replace("_tbd_", function_arn) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + lambda_200_string_body = MockedResponseLoader.load( + MockedResponseLoader.LAMBDA_200_STRING_BODY + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"ProcessValue": "lambda_200_string_body"}} + } + }, + "MockedResponses": {"lambda_200_string_body": lambda_200_string_body}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + definition = definition.replace( + "_tbd_", "arn:aws:lambda:us-east-1:111111111111:function:nosuchfunction" + ) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json index a930fbede3dc9..613e3a8d146f7 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json @@ -435,5 +435,1633 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_lambda_invoke": { + "recorded-date": "22-04-2025, 10:30:21", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "body": "string body" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "body": "string body" + }, + "inputDetails": { + "truncated": false + }, + "name": "step1" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "lambdaFunctionScheduledEventDetails": { + "input": { + "body": "string body" + }, + "inputDetails": { + "truncated": false + }, + "resource": "arn::lambda::111111111111:function:lambda_function_name" + }, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "LambdaFunctionScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "LambdaFunctionStarted" + }, + { + "id": 5, + "lambdaFunctionSucceededEventDetails": { + "output": { + "body": "string body" + }, + "outputDetails": { + "truncated": false + } + }, + "previousEventId": 4, + "timestamp": "timestamp", + "type": "LambdaFunctionSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "step1", + "output": { + "body": "string body" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "body": "string body" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_sqs_send_message": { + "recorded-date": "22-04-2025, 19:39:14", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs-queue-url", + "MessageBody": "test_message_body" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn::iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs-queue-url", + "MessageBody": "test_message_body" + }, + "inputDetails": { + "truncated": false + }, + "name": "SendSQS" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "QueueUrl": "sqs-queue-url", + "MessageBody": "test_message_body" + }, + "region": "", + "resource": "sendMessage", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "SendSQS", + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_sns_publish_base": { + "recorded-date": "23-04-2025, 13:52:23", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "TopicArn": "topic-arn", + "Message": { + "message": "string-literal" + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "TopicArn": "topic-arn", + "Message": { + "message": "string-literal" + } + }, + "inputDetails": { + "truncated": false + }, + "name": "Publish" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "TopicArn": "topic-arn", + "Message": { + "message": "string-literal" + } + }, + "region": "", + "resource": "publish", + "resourceType": "sns" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "publish", + "resourceType": "sns" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "294" + ], + "Date": "date", + "Content-Type": [ + "text/xml" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "294", + "Content-Type": "text/xml", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "publish", + "resourceType": "sns" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "Publish", + "output": { + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "294" + ], + "Date": "date", + "Content-Type": [ + "text/xml" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "294", + "Content-Type": "text/xml", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "294" + ], + "Date": "date", + "Content-Type": [ + "text/xml" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "294", + "Content-Type": "text/xml", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_events_put_events": { + "recorded-date": "23-04-2025, 14:28:24", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "Entries": [ + { + "Detail": "{\"Message\": \"string-literal\"}", + "DetailType": "detail-type", + "Source": "some.source" + } + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "Entries": [ + { + "Detail": "{\"Message\": \"string-literal\"}", + "DetailType": "detail-type", + "Source": "some.source" + } + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "PutEvents" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Entries": [ + { + "Detail": "{\"Message\": \"string-literal\"}", + "DetailType": "detail-type", + "Source": "some.source" + } + ] + }, + "region": "", + "resource": "putEvents", + "resourceType": "events" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "putEvents", + "resourceType": "events" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "Entries": [ + { + "EventId": "" + } + ], + "FailedEntryCount": 0 + }, + "outputDetails": { + "truncated": false + }, + "resource": "putEvents", + "resourceType": "events" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "PutEvents", + "output": { + "Entries": [ + { + "EventId": "" + } + ], + "FailedEntryCount": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Entries": [ + { + "EventId": "" + } + ], + "FailedEntryCount": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_dynamodb_put_get_item": { + "recorded-date": "23-04-2025, 15:32:30", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + } + }, + "inputDetails": { + "truncated": false + }, + "name": "PutItem" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + } + }, + "region": "", + "resource": "putItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "putItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "2" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "putItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "PutItem", + "output": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + }, + "putItemOutput": { + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "2" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": 7, + "previousEventId": 6, + "stateEnteredEventDetails": { + "input": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + }, + "putItemOutput": { + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "2" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + } + }, + "inputDetails": { + "truncated": false + }, + "name": "GetItem" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 8, + "previousEventId": 7, + "taskScheduledEventDetails": { + "parameters": { + "TableName": "table-name", + "Key": { + "id": { + "S": "id1" + } + } + }, + "region": "", + "resource": "getItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 9, + "previousEventId": 8, + "taskStartedEventDetails": { + "resource": "getItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 10, + "previousEventId": 9, + "taskSucceededEventDetails": { + "output": { + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "57" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "57", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "getItem", + "resourceType": "dynamodb" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 11, + "previousEventId": 10, + "stateExitedEventDetails": { + "name": "GetItem", + "output": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + }, + "putItemOutput": { + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "2" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "getItemOutput": { + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "57" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "57", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "TableName": "table-name", + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "Key": { + "id": { + "S": "id1" + } + }, + "putItemOutput": { + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "2" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "getItemOutput": { + "Item": { + "data": { + "S": "string-literal" + }, + "id": { + "S": "id1" + } + }, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "Server": [ + "Server" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "x-amz-crc32": "x-amz-crc32", + "Content-Length": [ + "57" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "57", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "Server": "Server", + "x-amz-crc32": "x-amz-crc32", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 12, + "previousEventId": 11, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_map_state_lambda": { + "recorded-date": "24-04-2025, 11:11:05", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "StartState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "StartState", + "output": { + "Iterations": 2, + "Values": [ + { + "body": "string body" + } + ] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Iterations": 2, + "Values": [ + { + "body": "string body" + } + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 6, + "previousEventId": 5, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 7, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 8, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 9, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 10, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 11, + "previousEventId": 8, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 12, + "previousEventId": 11, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 14, + "previousEventId": 13, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 15, + "previousEventId": 14, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 16, + "previousEventId": 15, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 17, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 16, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 18, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 17, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 19, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 20, + "previousEventId": 19, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 21, + "previousEventId": 18, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 22, + "previousEventId": 21, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 23, + "previousEventId": 22, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 24, + "previousEventId": 23, + "stateEnteredEventDetails": { + "input": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "Terminate" + }, + "timestamp": "timestamp", + "type": "SucceedStateEntered" + }, + { + "id": 25, + "previousEventId": 24, + "stateExitedEventDetails": { + "name": "Terminate", + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "SucceedStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Values": [ + { + "body": "string body" + } + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "id": 26, + "previousEventId": 25, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json index cc7c6bc4de8e5..fff3ec14317af 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json @@ -1,5 +1,23 @@ { + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_dynamodb_put_get_item": { + "last_validated_date": "2025-04-23T15:32:30+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_events_put_events": { + "last_validated_date": "2025-04-23T14:28:24+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_lambda_invoke": { + "last_validated_date": "2025-04-22T10:30:21+00:00" + }, "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_lambda_service_invoke": { "last_validated_date": "2025-04-14T18:51:50+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_map_state_lambda": { + "last_validated_date": "2025-04-24T11:11:05+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_sns_publish_base": { + "last_validated_date": "2025-04-23T13:52:23+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_sqs_send_message": { + "last_validated_date": "2025-04-22T19:39:14+00:00" } } From d80eef77f2647e92c7f9c7a7fd9a240959993ff4 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:38:56 +0200 Subject: [PATCH 087/108] Upgrade pinned Python dependencies (#12561) Co-authored-by: LocalStack Bot --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 8 ++++---- requirements-basic.txt | 2 +- requirements-dev.txt | 18 +++++++++--------- requirements-runtime.txt | 12 ++++++------ requirements-test.txt | 16 ++++++++-------- requirements-typehint.txt | 36 +++++++++++++++++------------------ 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ffd7c5259913..ce8be6924df84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index b3bb668e42b89..b43a075b4432f 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -9,7 +9,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -awscrt==0.26.1 +awscrt==0.27.0 # via localstack-core (pyproject.toml) boto3==1.38.0 # via localstack-core (pyproject.toml) @@ -24,7 +24,7 @@ cachetools==5.5.2 # via localstack-core (pyproject.toml) cbor2==5.6.5 # via localstack-core (pyproject.toml) -certifi==2025.1.31 +certifi==2025.4.26 # via requests cffi==1.17.1 # via cryptography @@ -46,7 +46,7 @@ dnspython==2.7.0 # via localstack-core (pyproject.toml) docker==7.1.0 # via localstack-core (pyproject.toml) -h11==0.14.0 +h11==0.16.0 # via # hypercorn # wsproto @@ -88,7 +88,7 @@ jsonschema-path==0.3.4 # via # openapi-core # openapi-spec-validator -jsonschema-specifications==2024.10.1 +jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator diff --git a/requirements-basic.txt b/requirements-basic.txt index a37daec3d90db..e70ca59967c46 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -8,7 +8,7 @@ build==1.2.2.post1 # via localstack-core (pyproject.toml) cachetools==5.5.2 # via localstack-core (pyproject.toml) -certifi==2025.1.31 +certifi==2025.4.26 # via requests cffi==1.17.1 # via cryptography diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d537a29c2646..e1e13d2578aa0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.191.0 +aws-cdk-lib==2.192.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.39.0 # via localstack-core -awscrt==0.26.1 +awscrt==0.27.0 # via localstack-core boto3==1.38.0 # via @@ -72,7 +72,7 @@ cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core -certifi==2025.1.31 +certifi==2025.4.26 # via # httpcore # httpx @@ -82,7 +82,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.34.1 +cfn-lint==1.34.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -148,7 +148,7 @@ filelock==3.18.0 # via virtualenv graphql-core==3.2.6 # via moto-ext -h11==0.14.0 +h11==0.16.0 # via # httpcore # hypercorn @@ -160,7 +160,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.8 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via localstack-core @@ -230,7 +230,7 @@ jsonschema-path==0.3.4 # via # openapi-core # openapi-spec-validator -jsonschema-specifications==2024.10.1 +jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator @@ -429,7 +429,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.11.6 +ruff==0.11.7 # via localstack-core (pyproject.toml) s3transfer==0.12.0 # via @@ -447,7 +447,7 @@ six==1.17.0 # rfc3339-validator sniffio==1.3.1 # via anyio -sympy==1.13.3 +sympy==1.14.0 # via cfn-lint tailer==0.4.1 # via diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 5d01d86822399..75f8e05e732e4 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -31,7 +31,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.39.0 # via localstack-core (pyproject.toml) -awscrt==0.26.1 +awscrt==0.27.0 # via localstack-core boto3==1.38.0 # via @@ -58,13 +58,13 @@ cachetools==5.5.2 # localstack-core (pyproject.toml) cbor2==5.6.5 # via localstack-core -certifi==2025.1.31 +certifi==2025.4.26 # via # opensearch-py # requests cffi==1.17.1 # via cryptography -cfn-lint==1.34.1 +cfn-lint==1.34.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -110,7 +110,7 @@ events==0.5 # via opensearch-py graphql-core==3.2.6 # via moto-ext -h11==0.14.0 +h11==0.16.0 # via # hypercorn # wsproto @@ -170,7 +170,7 @@ jsonschema-path==0.3.4 # via # openapi-core # openapi-spec-validator -jsonschema-specifications==2024.10.1 +jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator @@ -326,7 +326,7 @@ six==1.17.0 # jsonpath-rw # python-dateutil # rfc3339-validator -sympy==1.13.3 +sympy==1.14.0 # via cfn-lint tailer==0.4.1 # via diff --git a/requirements-test.txt b/requirements-test.txt index f7cf447cce7ab..a55221103742a 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.191.0 +aws-cdk-lib==2.192.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.97.0 # via @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.39.0 # via localstack-core -awscrt==0.26.1 +awscrt==0.27.0 # via localstack-core boto3==1.38.0 # via @@ -72,7 +72,7 @@ cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core -certifi==2025.1.31 +certifi==2025.4.26 # via # httpcore # httpx @@ -80,7 +80,7 @@ certifi==2025.1.31 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.34.1 +cfn-lint==1.34.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -134,7 +134,7 @@ events==0.5 # via opensearch-py graphql-core==3.2.6 # via moto-ext -h11==0.14.0 +h11==0.16.0 # via # httpcore # hypercorn @@ -146,7 +146,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.8 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via localstack-core (pyproject.toml) @@ -214,7 +214,7 @@ jsonschema-path==0.3.4 # via # openapi-core # openapi-spec-validator -jsonschema-specifications==2024.10.1 +jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator @@ -405,7 +405,7 @@ six==1.17.0 # rfc3339-validator sniffio==1.3.1 # via anyio -sympy==1.13.3 +sympy==1.14.0 # via cfn-lint tailer==0.4.1 # via diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 1070b7c41cc93..ae10a3ed31d61 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -33,7 +33,7 @@ aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.191.0 +aws-cdk-lib==2.192.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -43,7 +43,7 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.39.0 # via localstack-core -awscrt==0.26.1 +awscrt==0.27.0 # via localstack-core boto3==1.38.0 # via @@ -51,7 +51,7 @@ boto3==1.38.0 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.38.0 +boto3-stubs==1.38.4 # via localstack-core (pyproject.toml) botocore==1.38.0 # via @@ -61,7 +61,7 @@ botocore==1.38.0 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.38.0 +botocore-stubs==1.38.4 # via boto3-stubs build==1.2.2.post1 # via @@ -76,7 +76,7 @@ cattrs==24.1.3 # via jsii cbor2==5.6.5 # via localstack-core -certifi==2025.1.31 +certifi==2025.4.26 # via # httpcore # httpx @@ -86,7 +86,7 @@ cffi==1.17.1 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==1.34.1 +cfn-lint==1.34.2 # via moto-ext charset-normalizer==3.4.1 # via requests @@ -152,7 +152,7 @@ filelock==3.18.0 # via virtualenv graphql-core==3.2.6 # via moto-ext -h11==0.14.0 +h11==0.16.0 # via # httpcore # hypercorn @@ -164,7 +164,7 @@ h2==4.2.0 # localstack-twisted hpack==4.1.0 # via h2 -httpcore==1.0.8 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via localstack-core @@ -234,7 +234,7 @@ jsonschema-path==0.3.4 # via # openapi-core # openapi-spec-validator -jsonschema-specifications==2024.10.1 +jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator @@ -262,7 +262,7 @@ multipart==1.2.1 # via moto-ext mypy==1.15.0 # via localstack-core -mypy-boto3-acm==1.38.0 +mypy-boto3-acm==1.38.4 # via boto3-stubs mypy-boto3-acm-pca==1.38.0 # via boto3-stubs @@ -278,7 +278,7 @@ mypy-boto3-appconfigdata==1.38.0 # via boto3-stubs mypy-boto3-application-autoscaling==1.38.0 # via boto3-stubs -mypy-boto3-appsync==1.38.0 +mypy-boto3-appsync==1.38.2 # via boto3-stubs mypy-boto3-athena==1.38.0 # via boto3-stubs @@ -294,13 +294,13 @@ mypy-boto3-cloudcontrol==1.38.0 # via boto3-stubs mypy-boto3-cloudformation==1.38.0 # via boto3-stubs -mypy-boto3-cloudfront==1.38.0 +mypy-boto3-cloudfront==1.38.4 # via boto3-stubs mypy-boto3-cloudtrail==1.38.0 # via boto3-stubs mypy-boto3-cloudwatch==1.38.0 # via boto3-stubs -mypy-boto3-codebuild==1.38.0 +mypy-boto3-codebuild==1.38.2 # via boto3-stubs mypy-boto3-codecommit==1.38.0 # via boto3-stubs @@ -320,7 +320,7 @@ mypy-boto3-dms==1.38.0 # via boto3-stubs mypy-boto3-docdb==1.38.0 # via boto3-stubs -mypy-boto3-dynamodb==1.38.0 +mypy-boto3-dynamodb==1.38.4 # via boto3-stubs mypy-boto3-dynamodbstreams==1.38.0 # via boto3-stubs @@ -328,7 +328,7 @@ mypy-boto3-ec2==1.38.0 # via boto3-stubs mypy-boto3-ecr==1.38.0 # via boto3-stubs -mypy-boto3-ecs==1.38.0 +mypy-boto3-ecs==1.38.3 # via boto3-stubs mypy-boto3-efs==1.38.0 # via boto3-stubs @@ -410,7 +410,7 @@ mypy-boto3-qldb==1.38.0 # via boto3-stubs mypy-boto3-qldb-session==1.38.0 # via boto3-stubs -mypy-boto3-rds==1.38.0 +mypy-boto3-rds==1.38.2 # via boto3-stubs mypy-boto3-rds-data==1.38.0 # via boto3-stubs @@ -639,7 +639,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.11.6 +ruff==0.11.7 # via localstack-core s3transfer==0.12.0 # via @@ -657,7 +657,7 @@ six==1.17.0 # rfc3339-validator sniffio==1.3.1 # via anyio -sympy==1.13.3 +sympy==1.14.0 # via cfn-lint tailer==0.4.1 # via From 936c6970284f2456a713643bc6fae36e1a602659 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:26:41 +0200 Subject: [PATCH 088/108] Step Functions: Fix Mock Test for Multi-Region (#12562) --- .../stepfunctions/v2/mocking/test_base_scenarios.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py index e7a4736d79590..6267001f37700 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py @@ -2,6 +2,7 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer +import localstack.testing.config from localstack import config from localstack.aws.api.lambda_ import Runtime from localstack.aws.api.stepfunctions import HistoryEventType @@ -76,8 +77,11 @@ def test_lambda_invoke( } mock_config_file_path = mock_config_file(mock_config) monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + # Insert the test environment's region name into this mock ARN + # to maintain snapshot compatibility across multi-region tests. + test_region_name = localstack.testing.config.TEST_AWS_REGION_NAME template["States"]["step1"]["Resource"] = ( - f"arn:aws:lambda:us-east-1:111111111111:function:{function_name}" + f"arn:aws:lambda:{test_region_name}:111111111111:function:{function_name}" ) definition = json.dumps(template) create_and_record_mocked_execution( From d2ba498e053a23ffc22ebef19869ba9028df5e20 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Tue, 29 Apr 2025 12:32:49 +0100 Subject: [PATCH 089/108] CFn: add v2 tests to CI (#12556) --- .circleci/config.yml | 37 + .../services/cloudformation/v2/entities.py | 3 + .../services/cloudformation/v2/provider.py | 1 + .../cloudformation/api/test_changesets.py | 712 -- .../api/test_changesets.snapshot.json | 6785 ----------------- .../cloudformation/v2/test_change_sets.py | 715 +- .../v2/test_change_sets.snapshot.json | 4574 +++++++++++ .../v2/test_change_sets.validation.json | 30 + 8 files changed, 5358 insertions(+), 7499 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c5c9159e75a47..aec1098be52d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -522,6 +522,37 @@ jobs: - store_test_results: path: target/reports/ + itest-cfn-v2-engine-provider: + executor: ubuntu-machine-amd64 + working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> + steps: + - prepare-acceptance-tests + - attach_workspace: + at: /tmp/workspace + - prepare-testselection + - prepare-pytest-tinybird + - prepare-account-region-randomization + - run: + name: Test CloudFormation Engine v2 + environment: + PROVIDER_OVERRIDE_CLOUDFORMATION: "engine-v2" + TEST_PATH: "tests/aws/services/cloudformation/v2" + COVERAGE_ARGS: "-p" + # TODO: use docker-run-tests + command: | + COVERAGE_FILE="target/coverage/.coverage.cloudformation_v2.${CIRCLE_NODE_INDEX}" \ + PYTEST_ARGS="${TINYBIRD_PYTEST_ARGS}${TESTSELECTION_PYTEST_ARGS}--reruns 3 --junitxml=target/reports/cloudformation_v2.xml -o junit_suite_name='cloudformation_v2'" \ + make test-coverage + - persist_to_workspace: + root: + /tmp/workspace + paths: + - repo/target/coverage/ + - store_test_results: + path: target/reports/ + ######################### ## Parity Metrics Jobs ## ######################### @@ -890,6 +921,10 @@ workflows: requires: - preflight - test-selection + - itest-cfn-v2-engine-provider: + requires: + - preflight + - test-selection - unit-tests: requires: - preflight @@ -951,6 +986,7 @@ workflows: - itest-cloudwatch-v1-provider - itest-events-v1-provider - itest-ddb-v2-provider + - itest-cfn-v2-engine-provider - acceptance-tests-amd64 - acceptance-tests-arm64 - integration-tests-amd64 @@ -965,6 +1001,7 @@ workflows: - itest-cloudwatch-v1-provider - itest-events-v1-provider - itest-ddb-v2-provider + - itest-cfn-v2-engine-provider - acceptance-tests-amd64 - acceptance-tests-arm64 - integration-tests-amd64 diff --git a/localstack-core/localstack/services/cloudformation/v2/entities.py b/localstack-core/localstack/services/cloudformation/v2/entities.py index c3747dc44658b..31de16b69613e 100644 --- a/localstack-core/localstack/services/cloudformation/v2/entities.py +++ b/localstack-core/localstack/services/cloudformation/v2/entities.py @@ -40,6 +40,7 @@ class ResolvedResource(TypedDict): class Stack: stack_name: str parameters: list[Parameter] + change_set_id: str | None change_set_name: str | None status: StackStatus status_reason: StackStatusReason | None @@ -96,6 +97,7 @@ def set_stack_status(self, status: StackStatus, reason: StackStatusReason | None def describe_details(self) -> ApiStack: result = { + "ChangeSetId": self.change_set_id, "CreationTime": self.creation_time, "StackId": self.stack_id, "StackName": self.stack_name, @@ -197,6 +199,7 @@ def describe_details(self, include_property_values: bool) -> DescribeChangeSetOu result = { "Status": self.status, "ChangeSetType": self.change_set_type, + "ChangeSetId": self.change_set_id, "ChangeSetName": self.change_set_name, "ExecutionStatus": self.execution_status, "RollbackConfiguration": {}, diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 48bff856d3c87..f2a8afe509f19 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -223,6 +223,7 @@ def create_change_set( ) change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE) stack.change_set_id = change_set.change_set_id + stack.change_set_id = change_set.change_set_id state.change_sets[change_set.change_set_id] = change_set return CreateChangeSetOutput(StackId=stack.stack_id, Id=change_set.change_set_id) diff --git a/tests/aws/services/cloudformation/api/test_changesets.py b/tests/aws/services/cloudformation/api/test_changesets.py index 61480d44f05f0..1f397310f5d21 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.py +++ b/tests/aws/services/cloudformation/api/test_changesets.py @@ -4,7 +4,6 @@ import pytest from botocore.exceptions import ClientError -from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.aws.connect import ServiceLevelClientFactory from localstack.services.cloudformation.v2.utils import is_v2_engine @@ -1213,714 +1212,3 @@ def test_describe_change_set_with_similarly_named_stacks(deploy_cfn_template, aw )["ChangeSetId"] == response["Id"] ) - - -@pytest.mark.skipif( - condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" -) -@markers.snapshot.skip_snapshot_verify( - paths=[ - "per-resource-events..*", - "delete-describe..*", - # - "$..ChangeSetId", # An issue for the WIP executor - # Before/After Context - "$..Capabilities", - "$..NotificationARNs", - "$..IncludeNestedStacks", - "$..Scope", - "$..Details", - "$..Parameters", - "$..Replacement", - "$..PolicyAction", - "$..PhysicalResourceId", - ] -) -class TestCaptureUpdateProcess: - @markers.aws.validated - def test_direct_update( - self, - snapshot, - capture_update_process, - ): - """ - Update a stack with a static change (i.e. in the text of the template). - - Conclusions: - - A static change in the template that's not invoking an intrinsic function - (`Ref`, `Fn::GetAtt` etc.) is resolved by the deployment engine synchronously - during the `create_change_set` invocation - """ - name1 = f"topic-1-{short_uid()}" - name2 = f"topic-2-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-1")) - snapshot.add_transformer(RegexTransformer(name2, "topic-2")) - t1 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name1, - }, - }, - }, - } - t2 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name2, - }, - }, - }, - } - capture_update_process(snapshot, t1, t2) - - @markers.aws.validated - def test_dynamic_update( - self, - snapshot, - capture_update_process, - ): - """ - Update a stack with two resources: - - A is changed statically - - B refers to the changed value of A via an intrinsic function - - Conclusions: - - The value of B on creation is "known after apply" even though the resolved - property value is known statically - - The nature of the change to B is "known after apply" - - The CloudFormation engine does not resolve intrinsic function calls when determining the - nature of the update - """ - name1 = f"topic-1-{short_uid()}" - name2 = f"topic-2-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-1")) - snapshot.add_transformer(RegexTransformer(name2, "topic-2")) - t1 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name1, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - t2 = { - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name2, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - capture_update_process(snapshot, t1, t2) - - @markers.aws.validated - def test_parameter_changes( - self, - snapshot, - capture_update_process, - ): - """ - Update a stack with two resources: - - A is changed via a template parameter - - B refers to the changed value of A via an intrinsic function - - Conclusions: - - The value of B on creation is "known after apply" even though the resolved - property value is known statically - - The nature of the change to B is "known after apply" - - The CloudFormation engine does not resolve intrinsic function calls when determining the - nature of the update - """ - name1 = f"topic-1-{short_uid()}" - name2 = f"topic-2-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-1")) - snapshot.add_transformer(RegexTransformer(name2, "topic-2")) - t1 = { - "Parameters": { - "TopicName": { - "Type": "String", - }, - }, - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": {"Ref": "TopicName"}, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - capture_update_process(snapshot, t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) - - @markers.aws.validated - def test_mappings_with_static_fields( - self, - snapshot, - capture_update_process, - ): - """ - Update a stack with two resources: - - A is changed via looking up a static value in a mapping - - B refers to the changed value of A via an intrinsic function - - Conclusions: - - On first deploy the contents of the map is resolved completely - - The nature of the change to B is "known after apply" - - The CloudFormation engine does not resolve intrinsic function calls when determining the - nature of the update - """ - name1 = f"topic-1-{short_uid()}" - name2 = f"topic-2-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) - snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) - t1 = { - "Mappings": { - "MyMap": { - "MyKey": {"key1": name1, "key2": name2}, - }, - }, - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": { - "Fn::FindInMap": [ - "MyMap", - "MyKey", - "key1", - ], - }, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - t2 = { - "Mappings": { - "MyMap": { - "MyKey": { - "key1": name1, - "key2": name2, - }, - }, - }, - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": { - "Fn::FindInMap": [ - "MyMap", - "MyKey", - "key2", - ], - }, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - capture_update_process(snapshot, t1, t2) - - @markers.aws.validated - def test_mappings_with_parameter_lookup( - self, - snapshot, - capture_update_process, - ): - """ - Update a stack with two resources: - - A is changed via looking up a static value in a mapping but the key comes from - a template parameter - - B refers to the changed value of A via an intrinsic function - - Conclusions: - - The same conclusions as `test_mappings_with_static_fields` - """ - name1 = f"topic-1-{short_uid()}" - name2 = f"topic-2-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) - snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) - t1 = { - "Parameters": { - "TopicName": { - "Type": "String", - }, - }, - "Mappings": { - "MyMap": { - "MyKey": {"key1": name1, "key2": name2}, - }, - }, - "Resources": { - "Foo": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": { - "Fn::FindInMap": [ - "MyMap", - "MyKey", - { - "Ref": "TopicName", - }, - ], - }, - }, - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::GetAtt": ["Foo", "TopicName"], - }, - }, - }, - }, - } - capture_update_process(snapshot, t1, t1, p1={"TopicName": "key1"}, p2={"TopicName": "key2"}) - - @markers.aws.validated - def test_conditions( - self, - snapshot, - capture_update_process, - ): - """ - Toggle a resource from present to not present via a condition - - Conclusions: - - Adding the second resource creates an `Add` resource change - """ - t1 = { - "Parameters": { - "EnvironmentType": { - "Type": "String", - } - }, - "Conditions": { - "IsProduction": { - "Fn::Equals": [ - {"Ref": "EnvironmentType"}, - "prod", - ], - } - }, - "Resources": { - "Bucket": { - "Type": "AWS::S3::Bucket", - }, - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "test", - }, - "Condition": "IsProduction", - }, - }, - } - - capture_update_process( - snapshot, t1, t1, p1={"EnvironmentType": "not-prod"}, p2={"EnvironmentType": "prod"} - ) - - @markers.aws.validated - @pytest.mark.skip( - "Unlike AWS CFN, the update graph understands the dependent resource does not " - "need modification also when the IncludePropertyValues flag is off." - # TODO: we may achieve the same limitation by pruning the resolution of traversals. - ) - def test_unrelated_changes_update_propagation( - self, - snapshot, - capture_update_process, - ): - """ - - Resource B depends on resource A which is updated, but the referenced parameter does not - change - - Conclusions: - - No update to resource B - """ - topic_name = f"MyTopic{short_uid()}" - snapshot.add_transformer(RegexTransformer(topic_name, "topic-name")) - t1 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": topic_name, - "Description": "original", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - t2 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": topic_name, - "Description": "changed", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - capture_update_process(snapshot, t1, t2) - - @markers.aws.validated - @pytest.mark.skip( - "Deployment now succeeds but our describer incorrectly does not assign a change for Parameter2" - ) - def test_unrelated_changes_requires_replacement( - self, - snapshot, - capture_update_process, - ): - """ - - Resource B depends on resource A which is updated, but the referenced parameter does not - change, however resource A requires replacement - - Conclusions: - - Resource B is updated - """ - parameter_name_1 = f"MyParameter{short_uid()}" - parameter_name_2 = f"MyParameter{short_uid()}" - snapshot.add_transformer(RegexTransformer(parameter_name_1, "parameter-1-name")) - snapshot.add_transformer(RegexTransformer(parameter_name_2, "parameter-2-name")) - t1 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": parameter_name_1, - "Type": "String", - "Value": "value", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - t2 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": parameter_name_2, - "Type": "String", - "Value": "value", - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, - }, - }, - }, - } - capture_update_process(snapshot, t1, t2) - - @markers.aws.validated - @pytest.mark.parametrize( - "template", - [ - pytest.param( - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - } - }, - }, - id="change_dynamic", - ), - pytest.param( - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": "param-name", - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, - }, - }, - }, - }, - id="change_unrelated_property", - ), - pytest.param( - { - "Parameters": { - "ParameterValue": { - "Type": "String", - }, - }, - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Ref": "ParameterValue"}, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, - }, - }, - }, - }, - id="change_unrelated_property_not_create_only", - ), - pytest.param( - { - "Parameters": { - "ParameterValue": { - "Type": "String", - "Default": "value-1", - "AllowedValues": ["value-1", "value-2"], - } - }, - "Conditions": { - "ShouldCreateParameter": { - "Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"] - } - }, - "Resources": { - "SSMParameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - "SSMParameter2": { - "Type": "AWS::SSM::Parameter", - "Condition": "ShouldCreateParameter", - "Properties": { - "Type": "String", - "Value": "first", - }, - }, - }, - }, - id="change_parameter_for_condition_create_resource", - ), - ], - ) - def test_base_dynamic_parameter_scenarios( - self, snapshot, capture_update_process, template, request - ): - if request.node.callspec.id in { - "change_unrelated_property", - "change_unrelated_property_not_create_only", - }: - pytest.skip( - reason="AWS appears to incorrectly mark the dependent resource as needing update when describe " - "changeset is invoked without the inclusion of property values." - ) - capture_update_process( - snapshot, - template, - template, - {"ParameterValue": "value-1"}, - {"ParameterValue": "value-2"}, - ) - - @markers.aws.validated - def test_execute_with_ref(self, snapshot, aws_client, deploy_cfn_template): - name1 = f"param-1-{short_uid()}" - snapshot.add_transformer(snapshot.transform.regex(name1, "")) - name2 = f"param-2-{short_uid()}" - snapshot.add_transformer(snapshot.transform.regex(name2, "")) - value = "my-value" - param2_name = f"output-param-{short_uid()}" - snapshot.add_transformer(snapshot.transform.regex(param2_name, "")) - - t1 = { - "Resources": { - "Parameter1": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": name1, - "Type": "String", - "Value": value, - }, - }, - "Parameter2": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Name": param2_name, - "Type": "String", - "Value": {"Ref": "Parameter1"}, - }, - }, - } - } - t2 = copy.deepcopy(t1) - t2["Resources"]["Parameter1"]["Properties"]["Name"] = name2 - - stack = deploy_cfn_template(template=json.dumps(t1)) - stack_id = stack.stack_id - - before_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] - snapshot.match("before-value", before_value) - - deploy_cfn_template(stack_name=stack_id, template=json.dumps(t2), is_update=True) - - after_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] - snapshot.match("after-value", after_value) - - @markers.aws.validated - @pytest.mark.parametrize( - "template_1, template_2", - [ - ( - { - "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": [ - "GenericMapping", - "EnvironmentA", - "ParameterValue", - ] - }, - }, - } - }, - }, - { - "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-2"}}}, - "Resources": { - "MySSMParameter": { - "Type": "AWS::SSM::Parameter", - "Properties": { - "Type": "String", - "Value": { - "Fn::FindInMap": [ - "GenericMapping", - "EnvironmentA", - "ParameterValue", - ] - }, - }, - } - }, - }, - ) - ], - ids=["update_string_referencing_resource"], - ) - def test_base_mapping_scenarios( - self, - snapshot, - capture_update_process, - template_1, - template_2, - ): - capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json index ec3e3ec58f808..b3b80db8dd4fa 100644 --- a/tests/aws/services/cloudformation/api/test_changesets.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_changesets.snapshot.json @@ -498,6790 +498,5 @@ } } } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_direct_update": { - "recorded-date": "01-04-2025, 08:32:30", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [], - "LogicalResourceId": "Foo", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Foo", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "TopicName": "topic-2" - } - }, - "BeforeContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "topic-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Foo": [ - { - "EventId": "Foo-ce4449a5-3be2-4c7d-9737-e408c3ca7772", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-c0f9138e-b5fd-45c2-976e-f1fafee907d7", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_dynamic_update": { - "recorded-date": "01-04-2025, 12:30:53", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [], - "LogicalResourceId": "Foo", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Foo", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "TopicName": "topic-2" - } - }, - "BeforeContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "topic-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic-1", - "Type": "String" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Foo": [ - { - "EventId": "Foo-df917f95-bc5f-461d-9a94-80d49271b9c0", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-cccf775d-9ee4-4b65-afd3-68bd00248a16", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter": [ - { - "EventId": "Parameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "ResourceProperties": { - "Type": "String", - "Value": "topic-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "ResourceProperties": { - "Type": "String", - "Value": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-2lqqUTpVjPEC", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_parameter_changes": { - "recorded-date": "01-04-2025, 12:43:36", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [], - "LogicalResourceId": "Foo", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Foo", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "TopicName": "topic-2" - } - }, - "BeforeContext": { - "Properties": { - "TopicName": "topic-1" - } - }, - "Details": [ - { - "CausingEntity": "TopicName", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "AfterValue": "topic-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "topic-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic-1", - "Type": "String" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "TopicName", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Foo": [ - { - "EventId": "Foo-56f4760f-0517-4079-926e-6b9a0d48622e", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-9f0f56a9-9622-419d-bd2a-83e387b32c4b", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-2", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-1", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "", - "ResourceProperties": { - "TopicName": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter": [ - { - "EventId": "Parameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "ResourceProperties": { - "Type": "String", - "Value": "topic-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "ResourceProperties": { - "Type": "String", - "Value": "topic-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-4YW2lccyDs2E", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "topic-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "topic-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { - "recorded-date": "01-04-2025, 13:20:51", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "topic-name-1" - } - }, - "Details": [], - "LogicalResourceId": "Foo", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Foo", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "TopicName": "topic-name-2" - } - }, - "BeforeContext": { - "Properties": { - "TopicName": "topic-name-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "topic-name-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic-name-1", - "Type": "String" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Foo": [ - { - "EventId": "Foo-d832b7fd-78cf-4fba-a6fa-e440862a0428", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-19e3d928-52a1-4c4f-98ef-044bf9bb68e4", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter": [ - { - "EventId": "Parameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-oUbW72uDC2Ty", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_conditions": { - "recorded-date": "01-04-2025, 14:34:35", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": {} - }, - "Details": [], - "LogicalResourceId": "Bucket", - "Replacement": "True", - "ResourceType": "AWS::S3::Bucket", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "not-prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Bucket", - "ResourceType": "AWS::S3::Bucket", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "not-prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "not-prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "test", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Bucket": [ - { - "EventId": "Bucket-CREATE_COMPLETE-date", - "LogicalResourceId": "Bucket", - "PhysicalResourceId": "-bucket-fkzovuwylzkf", - "ResourceProperties": {}, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Bucket-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Bucket", - "PhysicalResourceId": "-bucket-fkzovuwylzkf", - "ResourceProperties": {}, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Bucket-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Bucket", - "PhysicalResourceId": "", - "ResourceProperties": {}, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::S3::Bucket", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter": [ - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-1xnvY3TGhTRc", - "ResourceProperties": { - "Type": "String", - "Value": "test" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-1xnvY3TGhTRc", - "ResourceProperties": { - "Type": "String", - "Value": "test" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "test" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "EnvironmentType", - "ParameterValue": "prod" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_update_propagation": { - "recorded-date": "01-04-2025, 16:40:03", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "topic-name", - "Type": "String", - "Description": "original" - } - }, - "Details": [], - "LogicalResourceId": "Parameter1", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter2", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter1", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter2", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "topic-name", - "Type": "String", - "Description": "changed" - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic-name", - "Type": "String", - "Description": "original" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "changed", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "original", - "Name": "Description", - "Path": "/Properties/Description", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Description", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Parameter1.Value", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Parameter1": [ - { - "EventId": "Parameter1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "ResourceProperties": { - "Type": "String", - "Description": "changed", - "Value": "topic-name" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "ResourceProperties": { - "Type": "String", - "Description": "changed", - "Value": "topic-name" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "ResourceProperties": { - "Type": "String", - "Description": "original", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UfDgbJkBI3OH", - "ResourceProperties": { - "Type": "String", - "Description": "original", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Description": "original", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter2": [ - { - "EventId": "Parameter2-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-SBSi3lbMjBeo", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_unrelated_changes_requires_replacement": { - "recorded-date": "01-04-2025, 16:46:22", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "value", - "Type": "String", - "Name": "parameter-1-name" - } - }, - "Details": [], - "LogicalResourceId": "Parameter1", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter2", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter1", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter2", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "value", - "Type": "String", - "Name": "parameter-2-name" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value", - "Type": "String", - "Name": "parameter-1-name" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "parameter-2-name", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "parameter-1-name", - "Name": "Name", - "Path": "/Properties/Name", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value", - "Type": "String" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "CausingEntity": "Parameter1.Value", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Name", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Parameter1.Value", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Parameter1": [ - { - "EventId": "Parameter1-ff8b6634-0c41-421e-93cc-6cc7d8dae415", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-399217af-3b0b-4d34-8220-daee4c9a3f59", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-2-name", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-2-name" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-2-name", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-2-name" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-2-name" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-1-name" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "parameter-1-name", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-1-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value", - "Name": "parameter-1-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter2": [ - { - "EventId": "Parameter2-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", - "ResourceProperties": { - "Type": "String", - "Value": "value" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-hXPMgTm4P162", - "ResourceProperties": { - "Type": "String", - "Value": "value" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { - "recorded-date": "01-04-2025, 13:31:33", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "topic-name-1" - } - }, - "Details": [], - "LogicalResourceId": "Foo", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Foo", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "TopicName": "topic-name-2" - } - }, - "BeforeContext": { - "Properties": { - "TopicName": "topic-name-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "topic-name-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - }, - { - "CausingEntity": "TopicName", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "AfterValue": "topic-name-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "topic-name-1", - "Type": "String" - } - }, - "Details": [ - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "TopicName", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } - } - ], - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Foo.TopicName", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Foo": [ - { - "EventId": "Foo-d149fa4f-7f1d-41a2-8db0-6bb7f7f78548", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceStatus": "DELETE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-34113190-b651-4ec5-84e2-b6324388325d", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceStatus": "DELETE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_COMPLETE-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Foo-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Foo", - "PhysicalResourceId": "", - "ResourceProperties": { - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter": [ - { - "EventId": "Parameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-WZ3e8kpJATT4", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "TopicName", - "ParameterValue": "key2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_dynamic]": { - "recorded-date": "03-04-2025, 07:11:45", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "value-2", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Parameter": [ - { - "EventId": "Parameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-9QtZNJQzYR1B", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property]": { - "recorded-date": "03-04-2025, 07:12:11", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "value-1", - "Type": "String", - "Name": "param-name" - } - }, - "Details": [], - "LogicalResourceId": "Parameter1", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter2", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter1", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter2", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "value-2", - "Type": "String", - "Name": "param-name" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value-1", - "Type": "String", - "Name": "param-name" - } - }, - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Parameter1.Name", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Parameter1": [ - { - "EventId": "Parameter1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "ResourceProperties": { - "Type": "String", - "Value": "value-2", - "Name": "param-name" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "ResourceProperties": { - "Type": "String", - "Value": "value-2", - "Name": "param-name" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "ResourceProperties": { - "Type": "String", - "Value": "value-1", - "Name": "param-name" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "param-name", - "ResourceProperties": { - "Type": "String", - "Value": "value-1", - "Name": "param-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value-1", - "Name": "param-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter2": [ - { - "EventId": "Parameter2-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", - "ResourceProperties": { - "Type": "String", - "Value": "param-name" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-jRTpA4b4WkBF", - "ResourceProperties": { - "Type": "String", - "Value": "param-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "param-name" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property_not_create_only]": { - "recorded-date": "03-04-2025, 07:12:37", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter1", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "Parameter2", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter1", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Parameter2", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "value-2", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "ParameterValue", - "ChangeSource": "ParameterReference", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "CausingEntity": "Parameter1.Type", - "ChangeSource": "ResourceAttribute", - "Evaluation": "Dynamic", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "Parameter1": [ - { - "EventId": "Parameter1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "CFN-Parameter1-UwGusLYvooSf", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter1", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Parameter2": [ - { - "EventId": "Parameter2-CREATE_COMPLETE-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", - "ResourceProperties": { - "Type": "String", - "Value": "String" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "CFN-Parameter2-QP9mHQJIkIP1", - "ResourceProperties": { - "Type": "String", - "Value": "String" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Parameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Parameter2", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "String" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_parameter_for_condition_create_resource]": { - "recorded-date": "03-04-2025, 07:13:01", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "first", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "SSMParameter1", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "SSMParameter1", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-1" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "first", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "SSMParameter2", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "SSMParameter2", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "SSMParameter1": [ - { - "EventId": "SSMParameter1-CREATE_COMPLETE-date", - "LogicalResourceId": "SSMParameter1", - "PhysicalResourceId": "CFN-SSMParameter1-Rk9HUEXJeXov", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "SSMParameter1", - "PhysicalResourceId": "CFN-SSMParameter1-Rk9HUEXJeXov", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "SSMParameter1", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "SSMParameter2": [ - { - "EventId": "SSMParameter2-CREATE_COMPLETE-date", - "LogicalResourceId": "SSMParameter2", - "PhysicalResourceId": "CFN-SSMParameter2-esnxaJTjOAZM", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "SSMParameter2", - "PhysicalResourceId": "CFN-SSMParameter2-esnxaJTjOAZM", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "SSMParameter2", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "first" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "Parameters": [ - { - "ParameterKey": "ParameterValue", - "ParameterValue": "value-2" - } - ], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_condition_scenarios[condition_update_create_resource]": { - "recorded-date": "03-04-2025, 07:16:52", - "recorded-content": {} - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_base_mapping_scenarios[update_string_referencing_resource]": { - "recorded-date": "03-04-2025, 07:23:48", - "recorded-content": { - "create-change-set-1": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [], - "LogicalResourceId": "MySSMParameter", - "Replacement": "True", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-1": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "MySSMParameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-1-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - }, - "create-change-set-2": { - "Id": "arn::cloudformation::111111111111:changeSet/", - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2-prop-values": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "Value": "value-2", - "Type": "String" - } - }, - "BeforeContext": { - "Properties": { - "Value": "value-1", - "Type": "String" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "value-2", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "value-1", - "Name": "Value", - "Path": "/Properties/Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-change-set-2": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "Value", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "Replacement": "False", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "execute-change-set-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "post-create-2-describe": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "UPDATE_COMPLETE", - "Tags": [] - }, - "per-resource-events": { - "MySSMParameter": [ - { - "EventId": "MySSMParameter-UPDATE_COMPLETE-date", - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "MySSMParameter-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "ResourceProperties": { - "Type": "String", - "Value": "value-2" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "MySSMParameter-CREATE_COMPLETE-date", - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "CFN-MySSMParameter-hDQTEDs80OuI", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "MySSMParameter", - "PhysicalResourceId": "", - "ResourceProperties": { - "Type": "String", - "Value": "value-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "": [ - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "", - "LogicalResourceId": "", - "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", - "ResourceStatus": "REVIEW_IN_PROGRESS", - "ResourceStatusReason": "User Initiated", - "ResourceType": "AWS::CloudFormation::Stack", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ] - }, - "delete-describe": { - "CreationTime": "datetime", - "DeletionTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "DELETE_COMPLETE", - "Tags": [] - } - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestCaptureUpdateProcess::test_execute_with_ref": { - "recorded-date": "11-04-2025, 14:34:15", - "recorded-content": { - "before-value": "", - "after-value": "" - } - }, - "tests/aws/services/cloudformation/api/test_changesets.py::TestUpdates::test_deleting_resource": { - "recorded-date": "15-04-2025, 15:07:18", - "recorded-content": { - "get-parameter-error": { - "Error": { - "Code": "ParameterNotFound", - "Message": "" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.py b/tests/aws/services/cloudformation/v2/test_change_sets.py index 3a86ac9cbbeeb..2bc1ebff01866 100644 --- a/tests/aws/services/cloudformation/v2/test_change_sets.py +++ b/tests/aws/services/cloudformation/v2/test_change_sets.py @@ -2,19 +2,730 @@ import json import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer -from localstack import config from localstack.aws.connect import ServiceLevelClientFactory +from localstack.services.cloudformation.v2.utils import is_v2_engine from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import short_uid pytestmark = pytest.mark.skipif( - not is_aws_cloud() and config.SERVICE_PROVIDER_CONFIG["cloudformation"] == "engine-v2", + condition=not is_v2_engine() and not is_aws_cloud(), reason="Only targeting the new engine", ) +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + "$..PhysicalResourceId", + ] +) +class TestCaptureUpdateProcess: + @markers.aws.validated + def test_direct_update( + self, + snapshot, + capture_update_process, + ): + """ + Update a stack with a static change (i.e. in the text of the template). + + Conclusions: + - A static change in the template that's not invoking an intrinsic function + (`Ref`, `Fn::GetAtt` etc.) is resolved by the deployment engine synchronously + during the `create_change_set` invocation + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) + t1 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + }, + }, + }, + } + t2 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + }, + }, + }, + } + capture_update_process(snapshot, t1, t2) + + @markers.aws.validated + def test_dynamic_update( + self, + snapshot, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed statically + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The value of B on creation is "known after apply" even though the resolved + property value is known statically + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) + t1 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + t2 = { + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name2, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + capture_update_process(snapshot, t1, t2) + + @markers.aws.validated + def test_parameter_changes( + self, + snapshot, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via a template parameter + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The value of B on creation is "known after apply" even though the resolved + property value is known statically + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-2")) + t1 = { + "Parameters": { + "TopicName": { + "Type": "String", + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": {"Ref": "TopicName"}, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + capture_update_process(snapshot, t1, t1, p1={"TopicName": name1}, p2={"TopicName": name2}) + + @markers.aws.validated + def test_mappings_with_static_fields( + self, + snapshot, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via looking up a static value in a mapping + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - On first deploy the contents of the map is resolved completely + - The nature of the change to B is "known after apply" + - The CloudFormation engine does not resolve intrinsic function calls when determining the + nature of the update + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + t1 = { + "Mappings": { + "MyMap": { + "MyKey": {"key1": name1, "key2": name2}, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + "key1", + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + t2 = { + "Mappings": { + "MyMap": { + "MyKey": { + "key1": name1, + "key2": name2, + }, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + "key2", + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + capture_update_process(snapshot, t1, t2) + + @markers.aws.validated + def test_mappings_with_parameter_lookup( + self, + snapshot, + capture_update_process, + ): + """ + Update a stack with two resources: + - A is changed via looking up a static value in a mapping but the key comes from + a template parameter + - B refers to the changed value of A via an intrinsic function + + Conclusions: + - The same conclusions as `test_mappings_with_static_fields` + """ + name1 = f"topic-1-{short_uid()}" + name2 = f"topic-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + snapshot.add_transformer(RegexTransformer(name2, "topic-name-2")) + t1 = { + "Parameters": { + "TopicName": { + "Type": "String", + }, + }, + "Mappings": { + "MyMap": { + "MyKey": {"key1": name1, "key2": name2}, + }, + }, + "Resources": { + "Foo": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::FindInMap": [ + "MyMap", + "MyKey", + { + "Ref": "TopicName", + }, + ], + }, + }, + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::GetAtt": ["Foo", "TopicName"], + }, + }, + }, + }, + } + capture_update_process(snapshot, t1, t1, p1={"TopicName": "key1"}, p2={"TopicName": "key2"}) + + @markers.aws.validated + def test_conditions( + self, + snapshot, + capture_update_process, + ): + """ + Toggle a resource from present to not present via a condition + + Conclusions: + - Adding the second resource creates an `Add` resource change + """ + t1 = { + "Parameters": { + "EnvironmentType": { + "Type": "String", + } + }, + "Conditions": { + "IsProduction": { + "Fn::Equals": [ + {"Ref": "EnvironmentType"}, + "prod", + ], + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "test", + }, + "Condition": "IsProduction", + }, + }, + } + + capture_update_process( + snapshot, t1, t1, p1={"EnvironmentType": "not-prod"}, p2={"EnvironmentType": "prod"} + ) + + @markers.aws.validated + @pytest.mark.skip( + "Unlike AWS CFN, the update graph understands the dependent resource does not " + "need modification also when the IncludePropertyValues flag is off." + # TODO: we may achieve the same limitation by pruning the resolution of traversals. + ) + def test_unrelated_changes_update_propagation( + self, + snapshot, + capture_update_process, + ): + """ + - Resource B depends on resource A which is updated, but the referenced parameter does not + change + + Conclusions: + - No update to resource B + """ + topic_name = f"MyTopic{short_uid()}" + snapshot.add_transformer(RegexTransformer(topic_name, "topic-name")) + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": topic_name, + "Description": "original", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + t2 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": topic_name, + "Description": "changed", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + capture_update_process(snapshot, t1, t2) + + @markers.aws.validated + @pytest.mark.skip( + "Deployment now succeeds but our describer incorrectly does not assign a change for Parameter2" + ) + def test_unrelated_changes_requires_replacement( + self, + snapshot, + capture_update_process, + ): + """ + - Resource B depends on resource A which is updated, but the referenced parameter does not + change, however resource A requires replacement + + Conclusions: + - Resource B is updated + """ + parameter_name_1 = f"MyParameter{short_uid()}" + parameter_name_2 = f"MyParameter{short_uid()}" + snapshot.add_transformer(RegexTransformer(parameter_name_1, "parameter-1-name")) + snapshot.add_transformer(RegexTransformer(parameter_name_2, "parameter-2-name")) + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name_1, + "Type": "String", + "Value": "value", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + t2 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": parameter_name_2, + "Type": "String", + "Value": "value", + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Value"]}, + }, + }, + }, + } + capture_update_process(snapshot, t1, t2) + + @markers.aws.validated + @pytest.mark.parametrize( + "template", + [ + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + } + }, + }, + id="change_dynamic", + ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": "param-name", + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Name"]}, + }, + }, + }, + }, + id="change_unrelated_property", + ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + }, + }, + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Ref": "ParameterValue"}, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": {"Fn::GetAtt": ["Parameter1", "Type"]}, + }, + }, + }, + }, + id="change_unrelated_property_not_create_only", + ), + pytest.param( + { + "Parameters": { + "ParameterValue": { + "Type": "String", + "Default": "value-1", + "AllowedValues": ["value-1", "value-2"], + } + }, + "Conditions": { + "ShouldCreateParameter": { + "Fn::Equals": [{"Ref": "ParameterValue"}, "value-2"] + } + }, + "Resources": { + "SSMParameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + "SSMParameter2": { + "Type": "AWS::SSM::Parameter", + "Condition": "ShouldCreateParameter", + "Properties": { + "Type": "String", + "Value": "first", + }, + }, + }, + }, + id="change_parameter_for_condition_create_resource", + ), + ], + ) + def test_base_dynamic_parameter_scenarios( + self, snapshot, capture_update_process, template, request + ): + if request.node.callspec.id in { + "change_unrelated_property", + "change_unrelated_property_not_create_only", + }: + pytest.skip( + reason="AWS appears to incorrectly mark the dependent resource as needing update when describe " + "changeset is invoked without the inclusion of property values." + ) + capture_update_process( + snapshot, + template, + template, + {"ParameterValue": "value-1"}, + {"ParameterValue": "value-2"}, + ) + + @markers.aws.validated + def test_execute_with_ref(self, snapshot, aws_client, deploy_cfn_template): + name1 = f"param-1-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(name1, "")) + name2 = f"param-2-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(name2, "")) + value = "my-value" + param2_name = f"output-param-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(param2_name, "")) + + t1 = { + "Resources": { + "Parameter1": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Type": "String", + "Value": value, + }, + }, + "Parameter2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": param2_name, + "Type": "String", + "Value": {"Ref": "Parameter1"}, + }, + }, + } + } + t2 = copy.deepcopy(t1) + t2["Resources"]["Parameter1"]["Properties"]["Name"] = name2 + + stack = deploy_cfn_template(template=json.dumps(t1)) + stack_id = stack.stack_id + + before_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] + snapshot.match("before-value", before_value) + + deploy_cfn_template(stack_name=stack_id, template=json.dumps(t2), is_update=True) + + after_value = aws_client.ssm.get_parameter(Name=param2_name)["Parameter"]["Value"] + snapshot.match("after-value", after_value) + + @markers.aws.validated + @pytest.mark.parametrize( + "template_1, template_2", + [ + ( + { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-1"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + "EnvironmentA", + "ParameterValue", + ] + }, + }, + } + }, + }, + { + "Mappings": {"GenericMapping": {"EnvironmentA": {"ParameterValue": "value-2"}}}, + "Resources": { + "MySSMParameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Fn::FindInMap": [ + "GenericMapping", + "EnvironmentA", + "ParameterValue", + ] + }, + }, + } + }, + }, + ) + ], + ids=["update_string_referencing_resource"], + ) + def test_base_mapping_scenarios( + self, + snapshot, + capture_update_process, + template_1, + template_2, + ): + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json index 5226f24647715..d799e38efd682 100644 --- a/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_sets.snapshot.json @@ -93,5 +93,4579 @@ "Version": 2 } } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_direct_update": { + "recorded-date": "24-04-2025, 17:00:59", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-8fa001c0-096c-4f9e-9aed-0c31f45ded09", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-57ec24a9-92bd-4f31-8d36-972323072283", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_dynamic_update": { + "recorded-date": "24-04-2025, 17:02:59", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-1", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-33c3e9d2-d059-45a8-a51e-33eaf1f08abc", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-5160f677-0c84-41ba-ab19-45a474a4b7bf", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-b4xwNWwXL1pX", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_parameter_changes": { + "recorded-date": "24-04-2025, 17:38:55", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-1", + "Type": "String" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-da242d34-1619-4128-b9a1-24ae25f05899", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-8aa7df32-a61d-4794-9f57-c33004142e46", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-2", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-1", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "ResourceProperties": { + "Type": "String", + "Value": "topic-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-59wvoXl3mFfy", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "topic-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { + "recorded-date": "24-04-2025, 17:40:57", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-name-1", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-19d3838e-f734-4c47-bbc3-ed5ce898ae7f", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-1d67606c-91cd-478e-aa7f-bb5f79834fe4", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-U4lqVSH21TIK", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { + "recorded-date": "24-04-2025, 17:42:57", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Foo", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "TopicName": "topic-name-2" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + }, + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "topic-name-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "topic-name-1", + "Type": "String" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "AfterValue": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "TopicName", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "Foo.TopicName", + "ChangeSource": "ResourceAttribute", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Foo": [ + { + "EventId": "Foo-4f6c54a4-1549-4bd7-97c4-dd0ecca23860", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-53ede9ba-f993-45dd-9b68-e31f406d95c2", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-2", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_COMPLETE-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Foo-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Foo", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-ir98heGTa0zR", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "TopicName", + "ParameterValue": "key2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_conditions": { + "recorded-date": "24-04-2025, 17:54:44", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": {} + }, + "Details": [], + "LogicalResourceId": "Bucket", + "ResourceType": "AWS::S3::Bucket", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Bucket", + "ResourceType": "AWS::S3::Bucket", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "not-prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "test", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Bucket": [ + { + "EventId": "Bucket-CREATE_COMPLETE-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "-bucket-lrfokvsfgf0f", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Bucket-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "-bucket-lrfokvsfgf0f", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Bucket-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Bucket", + "PhysicalResourceId": "", + "ResourceProperties": {}, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::S3::Bucket", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-XN7hqAZ0p5We", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-XN7hqAZ0p5We", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "test" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "EnvironmentType", + "ParameterValue": "prod" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_dynamic]": { + "recorded-date": "24-04-2025, 17:55:06", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "CausingEntity": "ParameterValue", + "ChangeSource": "ParameterReference", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Dynamic", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-UlYVEyGMt3Hh", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property]": { + "recorded-date": "24-04-2025, 17:55:06", + "recorded-content": {} + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_unrelated_property_not_create_only]": { + "recorded-date": "24-04-2025, 17:55:06", + "recorded-content": {} + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_parameter_for_condition_create_resource]": { + "recorded-date": "24-04-2025, 17:55:28", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "first", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "SSMParameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SSMParameter1", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "first", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "SSMParameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "SSMParameter2", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "SSMParameter1": [ + { + "EventId": "SSMParameter1-CREATE_COMPLETE-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "CFN-SSMParameter1-qGQrGgGvuC42", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "CFN-SSMParameter1-qGQrGgGvuC42", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter1", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "SSMParameter2": [ + { + "EventId": "SSMParameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "CFN-SSMParameter2-9KvTVovmiPsN", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "CFN-SSMParameter2-9KvTVovmiPsN", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "SSMParameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "SSMParameter2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "first" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "ParameterValue", + "ParameterValue": "value-2" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_execute_with_ref": { + "recorded-date": "24-04-2025, 17:55:57", + "recorded-content": { + "before-value": "", + "after-value": "" + } + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_mapping_scenarios[update_string_referencing_resource]": { + "recorded-date": "24-04-2025, 17:56:19", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "MySSMParameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "MySSMParameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "value-2", + "Type": "String" + } + }, + "BeforeContext": { + "Properties": { + "Value": "value-1", + "Type": "String" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "value-2", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "value-1", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "MySSMParameter": [ + { + "EventId": "MySSMParameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "ResourceProperties": { + "Type": "String", + "Value": "value-2" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_COMPLETE-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "CFN-MySSMParameter-sK4jajBbVCXk", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "MySSMParameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "MySSMParameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "value-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_sets.validation.json b/tests/aws/services/cloudformation/v2/test_change_sets.validation.json index db75052b3a4d9..c54186e955aea 100644 --- a/tests/aws/services/cloudformation/v2/test_change_sets.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_sets.validation.json @@ -1,4 +1,34 @@ { + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_dynamic]": { + "last_validated_date": "2025-04-24T17:55:06+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_dynamic_parameter_scenarios[change_parameter_for_condition_create_resource]": { + "last_validated_date": "2025-04-24T17:55:28+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_base_mapping_scenarios[update_string_referencing_resource]": { + "last_validated_date": "2025-04-24T17:56:19+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_conditions": { + "last_validated_date": "2025-04-24T17:54:44+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_direct_update": { + "last_validated_date": "2025-04-24T17:00:59+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_dynamic_update": { + "last_validated_date": "2025-04-24T17:02:59+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_execute_with_ref": { + "last_validated_date": "2025-04-24T17:55:52+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_mappings_with_parameter_lookup": { + "last_validated_date": "2025-04-24T17:42:57+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_mappings_with_static_fields": { + "last_validated_date": "2025-04-24T17:40:56+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_sets.py::TestCaptureUpdateProcess::test_parameter_changes": { + "last_validated_date": "2025-04-24T17:38:55+00:00" + }, "tests/aws/services/cloudformation/v2/test_change_sets.py::test_single_resource_static_update": { "last_validated_date": "2025-03-18T16:52:35+00:00" } From e3f68a8d32274708e6a2368b12353007f47faded Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:10:19 +0200 Subject: [PATCH 090/108] Step Functions: Mock Mode Improvements (#12560) --- .../service/state_task_service_callback.py | 23 +- .../stepfunctions/asl/eval/environment.py | 10 +- .../stepfunctions/mocking/mock_config.py | 6 +- .../stepfunctions/mocking/mock_config_file.py | 53 ++- .../services/stepfunctions/provider.py | 4 +- .../testing/pytest/stepfunctions/utils.py | 46 +- .../__init__.py | 0 .../mock_config_files}/__init__.py | 0 .../lambda_sqs_integration.json5 | 72 +++ .../mocked_responses}/__init__.py | 0 .../mocked_responses/callback}/__init__.py | 0 .../callback/task_failure.json5 | 8 + .../task_success_string_literal.json5 | 5 + .../dynamodb/200_get_item.json5 | 0 .../dynamodb/200_put_item.json5 | 0 .../mocked_responses/dynamodb}/__init__.py | 0 .../events/200_put_events.json5 | 0 .../mocked_responses/events}/__init__.py | 0 .../lambda/200_string_body.json5 | 0 .../mocked_responses/lambda}/__init__.py | 0 .../not_ready_timeout_200_string_body.json5 | 0 .../mocked_responses/sns/200_publish.json5 | 0 .../mocked_responses/sns}/__init__.py | 0 .../sqs/200_send_message.json5 | 0 .../mocked_responses/sqs/__init__.py | 0 .../states/200_start_execution_sync.json5 | 0 .../states/200_start_execution_sync2.json5 | 0 .../mocked_responses/states/__init__.py | 0 .../mocked_service_integrations.py} | 30 +- .../templates/mocked/__init__.py | 0 .../templates/mocked/mocked_templates.py | 12 + .../mocked/statemachines/__init__.py | 0 .../lambda_sqs_integration.json5 | 37 ++ .../scenarios/scenarios_templates.py | 3 + .../parallel_state_service_lambda.json5 | 39 ++ .../lambdafunctions/return_decorated_input.py | 2 + .../templates/services/services_templates.py | 3 + .../v2/mocking/test_aws_scenarios.py | 156 +++++++ .../v2/mocking/test_base_callbacks.py | 167 ++++++- .../mocking/test_base_callbacks.snapshot.json | 398 +++++++++++++++++ .../test_base_callbacks.validation.json | 6 + .../v2/mocking/test_base_scenarios.py | 138 +++++- .../mocking/test_base_scenarios.snapshot.json | 412 ++++++++++++++++++ .../test_base_scenarios.validation.json | 3 + .../v2/mocking/test_mock_config_file.py | 8 +- 45 files changed, 1571 insertions(+), 70 deletions(-) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses => mocked_service_integrations/mock_config_files}/__init__.py (100%) create mode 100644 tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/lambda_sqs_integration.json5 rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/dynamodb => mocked_service_integrations/mocked_responses}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/events => mocked_service_integrations/mocked_responses/callback}/__init__.py (100%) create mode 100644 tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_failure.json5 create mode 100644 tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_success_string_literal.json5 rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/dynamodb/200_get_item.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/dynamodb/200_put_item.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/lambda => mocked_service_integrations/mocked_responses/dynamodb}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/events/200_put_events.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/sns => mocked_service_integrations/mocked_responses/events}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/lambda/200_string_body.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/sqs => mocked_service_integrations/mocked_responses/lambda}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/sns/200_publish.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses/mocked_responses/states => mocked_service_integrations/mocked_responses/sns}/__init__.py (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/sqs/200_send_message.json5 (100%) create mode 100644 tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sqs/__init__.py rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/states/200_start_execution_sync.json5 (100%) rename tests/aws/services/stepfunctions/{mocked_responses => mocked_service_integrations}/mocked_responses/states/200_start_execution_sync2.json5 (100%) create mode 100644 tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/__init__.py rename tests/aws/services/stepfunctions/{mocked_responses/mocked_response_loader.py => mocked_service_integrations/mocked_service_integrations.py} (50%) create mode 100644 tests/aws/services/stepfunctions/templates/mocked/__init__.py create mode 100644 tests/aws/services/stepfunctions/templates/mocked/mocked_templates.py create mode 100644 tests/aws/services/stepfunctions/templates/mocked/statemachines/__init__.py create mode 100644 tests/aws/services/stepfunctions/templates/mocked/statemachines/lambda_sqs_integration.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_service_lambda.json5 create mode 100644 tests/aws/services/stepfunctions/templates/services/lambdafunctions/return_decorated_input.py create mode 100644 tests/aws/services/stepfunctions/v2/mocking/test_aws_scenarios.py diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index 8b9f56720fa08..bed6e8b78fdd5 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -176,18 +176,12 @@ def _eval_integration_pattern( outcome: CallbackOutcome | Any try: if self.resource.condition == ResourceCondition.WaitForTaskToken: - # WaitForTaskToken workflows are evaluated the same way, - # whether running in mock mode or not. outcome = self._eval_wait_for_task_token( env=env, timeout_seconds=timeout_seconds, callback_endpoint=callback_endpoint, heartbeat_endpoint=heartbeat_endpoint, ) - elif env.is_mocked_mode(): - # Sync operation in mock mode: sync and sync2 workflows are skipped and the outcome - # treated as the overall operation output. - outcome = task_output else: # Sync operations require the task output as input. env.stack.append(task_output) @@ -334,6 +328,9 @@ def _after_eval_execution( normalised_parameters: dict, state_credentials: StateCredentials, ) -> None: + # TODO: In Mock mode, when simulating a failure, the mock response is handled by + # super()._eval_execution, so this block is never executed. Consequently, the + # "TaskSubmitted" event isn’t recorded in the event history. if self._is_integration_pattern(): output = env.stack[-1] env.event_manager.add_event( @@ -348,13 +345,13 @@ def _after_eval_execution( ) ), ) - self._eval_integration_pattern( - env=env, - resource_runtime_part=resource_runtime_part, - normalised_parameters=normalised_parameters, - state_credentials=state_credentials, - ) - + if not env.is_mocked_mode(): + self._eval_integration_pattern( + env=env, + resource_runtime_part=resource_runtime_part, + normalised_parameters=normalised_parameters, + state_credentials=state_credentials, + ) super()._after_eval_execution( env=env, resource_runtime_part=resource_runtime_part, diff --git a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py index 735276c68c2ff..ecb90be5b8d07 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py +++ b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py @@ -270,7 +270,15 @@ def is_standard_workflow(self) -> bool: return self.execution_type == StateMachineType.STANDARD def is_mocked_mode(self) -> bool: - return self.mock_test_case is not None + """ + Returns True if the state machine is running in mock mode and the current + state has a defined mock configuration in the target environment or frame; + otherwise, returns False. + """ + return ( + self.mock_test_case is not None + and self.next_state_name in self.mock_test_case.state_mocked_responses + ) def get_current_mocked_response(self) -> MockedResponse: if not self.is_mocked_mode(): diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py index 3c3c64e03300f..25f71acee35d5 100644 --- a/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config.py @@ -29,9 +29,9 @@ def __init__(self, range_start: int, range_end: int): class MockedResponseReturn(MockedResponse): - payload: Final[dict[Any, Any]] + payload: Final[Any] - def __init__(self, range_start: int, range_end: int, payload: dict[Any, Any]): + def __init__(self, range_start: int, range_end: int, payload: Any): super().__init__(range_start=range_start, range_end=range_end) self.payload = payload @@ -69,7 +69,7 @@ def __init__( "Mock responses must be consecutively numbered. " f"Expected the next response to begin at event {last_range_end + 1}." ) - repeats = mocked_response.range_start - mocked_response.range_end + 1 + repeats = mocked_response.range_end - mocked_response.range_start + 1 self.mocked_responses.extend([mocked_response] * repeats) last_range_end = mocked_response.range_end diff --git a/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py b/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py index c33578f330649..145ffd20750a2 100644 --- a/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py +++ b/localstack-core/localstack/services/stepfunctions/mocking/mock_config_file.py @@ -1,9 +1,10 @@ import logging import os from functools import lru_cache -from typing import Dict, Final, Optional +from json import JSONDecodeError +from typing import Any, Dict, Final, Optional -from pydantic import BaseModel, RootModel, model_validator +from pydantic import BaseModel, RootModel, ValidationError, model_validator from localstack import config @@ -13,13 +14,13 @@ _THROW_KEY: Final[str] = "Throw" -class RawReturnResponse(BaseModel): +class RawReturnResponse(RootModel[Any]): """ Represents a return response. Accepts any fields. """ - model_config = {"extra": "allow", "frozen": True} + model_config = {"frozen": True} class RawThrowResponse(BaseModel): @@ -122,12 +123,48 @@ def _read_sfn_raw_mock_config(file_path: str, modified_epoch: int) -> Optional[R mock_config_str = df.read() mock_config: RawMockConfig = RawMockConfig.model_validate_json(mock_config_str) return mock_config - except Exception as ex: - LOG.warning( - "Unable to load step functions mock configuration file at '%s' due to %s", + except (OSError, IOError) as file_error: + LOG.error("Failed to open mock configuration file '%s'. Error: %s", file_path, file_error) + return None + except ValidationError as validation_error: + errors = validation_error.errors() + if not errors: + # No detailed errors provided by Pydantic + LOG.error( + "Validation failed for mock configuration file at '%s'. " + "The file must contain a valid mock configuration.", + file_path, + ) + else: + for err in errors: + location = ".".join(str(loc) for loc in err["loc"]) + message = err["msg"] + error_type = err["type"] + LOG.error( + "Mock configuration file error at '%s': %s (%s)", + location, + message, + error_type, + ) + # TODO: add tests to ensure the hot-reloading of the mock configuration + # file works as expected, and inform the user with the info below: + # LOG.info( + # "Changes to the mock configuration file will be applied at the " + # "next mock execution without requiring a LocalStack restart." + # ) + return None + except JSONDecodeError as json_error: + LOG.error( + "Malformed JSON in mock configuration file at '%s'. Error: %s", file_path, - ex, + json_error, ) + # TODO: add tests to ensure the hot-reloading of the mock configuration + # file works as expected, and inform the user with the info below: + # LOG.info( + # "Changes to the mock configuration file will be applied at the " + # "next mock execution without requiring a LocalStack restart." + # ) return None diff --git a/localstack-core/localstack/services/stepfunctions/provider.py b/localstack-core/localstack/services/stepfunctions/provider.py index 95c04b4887e71..c43fd396c9a8f 100644 --- a/localstack-core/localstack/services/stepfunctions/provider.py +++ b/localstack-core/localstack/services/stepfunctions/provider.py @@ -851,7 +851,9 @@ def start_execution( if mock_test_case is None: raise InvalidName( f"Invalid mock test case name '{mock_test_case_name}' " - f"for state machine '{state_machine_name}'" + f"for state machine '{state_machine_name}'." + "Either the test case is not defined or the mock configuration file " + "could not be loaded. See logs for details." ) execution = Execution( diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py index cd55ef0106aa5..3b2925e5a9353 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py @@ -10,6 +10,7 @@ TransformContext, ) +from localstack import config from localstack.aws.api.stepfunctions import ( Arn, CloudWatchLogsLogGroup, @@ -543,7 +544,6 @@ def launch_and_record_logs( sfn_snapshot.match("logged_execution_events", logged_execution_events) -# TODO: make this return the execution ARN for manual assertions def create_and_record_execution( target_aws_client, create_state_machine_iam_role, @@ -552,7 +552,7 @@ def create_and_record_execution( definition, execution_input, verify_execution_description=False, -): +) -> LongArn: state_machine_arn = create_state_machine_with_iam_role( target_aws_client, create_state_machine_iam_role, @@ -560,13 +560,14 @@ def create_and_record_execution( sfn_snapshot, definition, ) - launch_and_record_execution( + exeuction_arn = launch_and_record_execution( target_aws_client, sfn_snapshot, state_machine_arn, execution_input, verify_execution_description, ) + return exeuction_arn def create_and_record_mocked_execution( @@ -578,7 +579,7 @@ def create_and_record_mocked_execution( execution_input, state_machine_name, test_name, -): +) -> LongArn: state_machine_arn = create_state_machine_with_iam_role( target_aws_client, create_state_machine_iam_role, @@ -587,9 +588,44 @@ def create_and_record_mocked_execution( definition, state_machine_name=state_machine_name, ) - launch_and_record_mocked_execution( + execution_arn = launch_and_record_mocked_execution( target_aws_client, sfn_snapshot, state_machine_arn, execution_input, test_name ) + return execution_arn + + +def create_and_run_mock( + target_aws_client, + monkeypatch, + mock_config_file, + mock_config: dict, + state_machine_name: str, + definition_template: dict, + execution_input: str, + test_name: str, +): + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + + sfn_client = target_aws_client.stepfunctions + + state_machine_name: str = state_machine_name or f"mocked_statemachine_{short_uid()}" + definition = json.dumps(definition_template) + creation_response = sfn_client.create_state_machine( + name=state_machine_name, + definition=definition, + roleArn="arn:aws:iam::111111111111:role/mock-role/mocked-run", + ) + state_machine_arn = creation_response["stateMachineArn"] + + test_case_arn = f"{state_machine_arn}#{test_name}" + execution = sfn_client.start_execution(stateMachineArn=test_case_arn, input=execution_input) + execution_arn = execution["executionArn"] + + await_execution_terminated(stepfunctions_client=sfn_client, execution_arn=execution_arn) + sfn_client.delete_state_machine(stateMachineArn=state_machine_arn) + + return execution_arn def create_and_record_logs( diff --git a/tests/aws/services/stepfunctions/mocked_responses/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/lambda_sqs_integration.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/lambda_sqs_integration.json5 new file mode 100644 index 0000000000000..7601785e51586 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_service_integrations/mock_config_files/lambda_sqs_integration.json5 @@ -0,0 +1,72 @@ +// Source: https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-test-sm-exec.html, April 2025 +{ + "StateMachines": { + "LambdaSQSIntegration": { + "TestCases": { + "HappyPath": { + "LambdaState": "MockedLambdaSuccess", + "SQSState": "MockedSQSSuccess" + }, + "RetryPath": { + "LambdaState": "MockedLambdaRetry", + "SQSState": "MockedSQSSuccess" + }, + "HybridPath": { + "LambdaState": "MockedLambdaSuccess" + } + } + } + }, + "MockedResponses": { + "MockedLambdaSuccess": { + "0": { + "Return": { + "StatusCode": 200, + "Payload": { + "StatusCode": 200, + "body": "Hello from Lambda!" + } + } + } + }, + "LambdaMockedResourceNotReady": { + "0": { + "Throw": { + "Error": "Lambda.ResourceNotReadyException", + "Cause": "Lambda resource is not ready." + } + } + }, + "MockedSQSSuccess": { + "0": { + "Return": { + "MD5OfMessageBody": "3bcb6e8e-7h85-4375-b0bc-1a59812c6e51", + "MessageId": "3bcb6e8e-8b51-4375-b0bc-1a59812c6e51" + } + } + }, + "MockedLambdaRetry": { + "0": { + "Throw": { + "Error": "Lambda.ResourceNotReadyException", + "Cause": "Lambda resource is not ready." + } + }, + "1-2": { + "Throw": { + "Error": "Lambda.TimeoutException", + "Cause": "Lambda timed out." + } + }, + "3": { + "Return": { + "StatusCode": 200, + "Payload": { + "StatusCode": 200, + "body": "Hello from Lambda!" + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_failure.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_failure.json5 new file mode 100644 index 0000000000000..8c31560fbc3cc --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_failure.json5 @@ -0,0 +1,8 @@ +{ + "0": { + "Throw": { + "Error": "Failure error", + "Cause": "Failure cause", + } + } +} diff --git a/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_success_string_literal.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_success_string_literal.json5 new file mode 100644 index 0000000000000..4cb6cb2f16e39 --- /dev/null +++ b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/callback/task_success_string_literal.json5 @@ -0,0 +1,5 @@ +{ + "0": { + "Return": "string-literal" + } +} diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/200_get_item.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_get_item.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/200_get_item.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/200_put_item.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/dynamodb/200_put_item.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/200_put_item.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/dynamodb/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/events/200_put_events.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/events/200_put_events.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/events/200_put_events.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/events/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/events/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/200_string_body.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/200_string_body.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/200_string_body.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/lambda/not_ready_timeout_200_string_body.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sns/200_publish.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sns/200_publish.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sns/200_publish.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sns/__init__.py similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/__init__.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sns/__init__.py diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sqs/200_send_message.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/sqs/200_send_message.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sqs/200_send_message.json5 diff --git a/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sqs/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/sqs/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/200_start_execution_sync.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/200_start_execution_sync.json5 diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/200_start_execution_sync2.json5 similarity index 100% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_responses/states/200_start_execution_sync2.json5 rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/200_start_execution_sync2.json5 diff --git a/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/__init__.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_responses/states/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_service_integrations.py similarity index 50% rename from tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py rename to tests/aws/services/stepfunctions/mocked_service_integrations/mocked_service_integrations.py index 193e2d7d44a5f..2ab9f76a13f9a 100644 --- a/tests/aws/services/stepfunctions/mocked_responses/mocked_response_loader.py +++ b/tests/aws/services/stepfunctions/mocked_service_integrations/mocked_service_integrations.py @@ -9,34 +9,44 @@ _LOAD_CACHE: Final[dict[str, dict]] = dict() -class MockedResponseLoader(abc.ABC): - LAMBDA_200_STRING_BODY: Final[str] = os.path.join( +class MockedServiceIntegrationsLoader(abc.ABC): + MOCKED_RESPONSE_LAMBDA_200_STRING_BODY: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/lambda/200_string_body.json5" ) - LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY: Final[str] = os.path.join( + MOCKED_RESPONSE_LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/lambda/not_ready_timeout_200_string_body.json5" ) - SQS_200_SEND_MESSAGE: Final[str] = os.path.join( + MOCKED_RESPONSE_SQS_200_SEND_MESSAGE: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/sqs/200_send_message.json5" ) - SNS_200_PUBLISH: Final[str] = os.path.join( + MOCKED_RESPONSE_SNS_200_PUBLISH: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/sns/200_publish.json5" ) - EVENTS_200_PUT_EVENTS: Final[str] = os.path.join( + MOCKED_RESPONSE_EVENTS_200_PUT_EVENTS: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/events/200_put_events.json5" ) - DYNAMODB_200_PUT_ITEM: Final[str] = os.path.join( + MOCKED_RESPONSE_DYNAMODB_200_PUT_ITEM: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/dynamodb/200_put_item.json5" ) - DYNAMODB_200_GET_ITEM: Final[str] = os.path.join( + MOCKED_RESPONSE_DYNAMODB_200_GET_ITEM: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/dynamodb/200_get_item.json5" ) - STATES_200_START_EXECUTION_SYNC: Final[str] = os.path.join( + MOCKED_RESPONSE_STATES_200_START_EXECUTION_SYNC: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/states/200_start_execution_sync.json5" ) - STATES_200_START_EXECUTION_SYNC2: Final[str] = os.path.join( + MOCKED_RESPONSE_STATES_200_START_EXECUTION_SYNC2: Final[str] = os.path.join( _THIS_FOLDER, "mocked_responses/states/200_start_execution_sync2.json5" ) + MOCKED_RESPONSE_CALLBACK_TASK_SUCCESS_STRING_LITERAL: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/callback/task_success_string_literal.json5" + ) + MOCKED_RESPONSE_CALLBACK_TASK_FAILURE: Final[str] = os.path.join( + _THIS_FOLDER, "mocked_responses/callback/task_failure.json5" + ) + + MOCK_CONFIG_FILE_LAMBDA_SQS_INTEGRATION: Final[str] = os.path.join( + _THIS_FOLDER, "mock_config_files/lambda_sqs_integration.json5" + ) @staticmethod def load(file_path: str) -> dict: diff --git a/tests/aws/services/stepfunctions/templates/mocked/__init__.py b/tests/aws/services/stepfunctions/templates/mocked/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/templates/mocked/mocked_templates.py b/tests/aws/services/stepfunctions/templates/mocked/mocked_templates.py new file mode 100644 index 0000000000000..6603956558911 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/mocked/mocked_templates.py @@ -0,0 +1,12 @@ +import os +from typing import Final + +from tests.aws.services.stepfunctions.templates.template_loader import TemplateLoader + +_THIS_FOLDER: Final[str] = os.path.dirname(os.path.realpath(__file__)) + + +class MockedTemplates(TemplateLoader): + LAMBDA_SQS_INTEGRATION: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/lambda_sqs_integration.json5" + ) diff --git a/tests/aws/services/stepfunctions/templates/mocked/statemachines/__init__.py b/tests/aws/services/stepfunctions/templates/mocked/statemachines/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/templates/mocked/statemachines/lambda_sqs_integration.json5 b/tests/aws/services/stepfunctions/templates/mocked/statemachines/lambda_sqs_integration.json5 new file mode 100644 index 0000000000000..466f000e8dafb --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/mocked/statemachines/lambda_sqs_integration.json5 @@ -0,0 +1,37 @@ +// Source: https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-test-sm-exec.html, April 2025 +{ + "Comment": "This state machine is called: LambdaSQSIntegration", + "StartAt": "LambdaState", + "States": { + "LambdaState": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "Payload.$": "$", + "FunctionName": "HelloWorldFunction" + }, + "Retry": [ + { + "ErrorEquals": [ + "States.ALL" + ], + // The aws demo calls for "MaxAttempts: 3" and 4 retry outcomes in "RetryPath" test case. + // However, through snapshot testing, we know that this is 1 too many retry outcomes for + // this definition. Hence, in an effort to keep parity with AWS Step Functions, the + // attempts numbers was adjusted to 4. + "MaxAttempts": 4 + } + ], + "Next": "SQSState" + }, + "SQSState": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage", + "Parameters": { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/account-id/myQueue", + "MessageBody.$": "$" + }, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index bab67189ca3dd..29a4a77473035 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -33,6 +33,9 @@ class ScenariosTemplate(TemplateLoader): PARALLEL_STATE_ORDER: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/parallel_state_order.json5" ) + PARALLEL_STATE_SERVICE_LAMBDA: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/parallel_state_service_lambda.json5" + ) MAP_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/map_state.json5") MAP_STATE_LEGACY: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_legacy.json5" diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_service_lambda.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_service_lambda.json5 new file mode 100644 index 0000000000000..11bef9148c5e7 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_service_lambda.json5 @@ -0,0 +1,39 @@ +{ + "StartAt": "ParallelState", + "States": { + "ParallelState": { + "Type": "Parallel", + "End": true, + "Branches": [ + { + "StartAt": "Branch1", + "States": { + "Branch1": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName.$": "$.FunctionNameBranch1", + "Payload.$": "$.Payload" + }, + "End": true + } + } + }, + { + "StartAt": "Branch2", + "States": { + "Branch2": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName.$": "$.FunctionNameBranch2", + "Payload.$": "$.Payload" + }, + "End": true + } + } + } + ] + } + } +} diff --git a/tests/aws/services/stepfunctions/templates/services/lambdafunctions/return_decorated_input.py b/tests/aws/services/stepfunctions/templates/services/lambdafunctions/return_decorated_input.py new file mode 100644 index 0000000000000..b02e0471301df --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/services/lambdafunctions/return_decorated_input.py @@ -0,0 +1,2 @@ +def handler(event, context): + return f"input-event-{event}" diff --git a/tests/aws/services/stepfunctions/templates/services/services_templates.py b/tests/aws/services/stepfunctions/templates/services/services_templates.py index d8ab0e7ece271..847acd6185a0e 100644 --- a/tests/aws/services/stepfunctions/templates/services/services_templates.py +++ b/tests/aws/services/stepfunctions/templates/services/services_templates.py @@ -109,3 +109,6 @@ class ServicesTemplates(TemplateLoader): LAMBDA_RETURN_BYTES_STR: Final[str] = os.path.join( _THIS_FOLDER, "lambdafunctions/return_bytes_str.py" ) + LAMBDA_RETURN_DECORATED_INPUT: Final[str] = os.path.join( + _THIS_FOLDER, "lambdafunctions/return_decorated_input.py" + ) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_aws_scenarios.py b/tests/aws/services/stepfunctions/v2/mocking/test_aws_scenarios.py new file mode 100644 index 0000000000000..0ced66200e798 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/mocking/test_aws_scenarios.py @@ -0,0 +1,156 @@ +import json + +from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import create_and_run_mock +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.mocked_service_integrations.mocked_service_integrations import ( + MockedServiceIntegrationsLoader, +) +from tests.aws.services.stepfunctions.templates.mocked.mocked_templates import MockedTemplates + + +class TestBaseScenarios: + @markers.aws.only_localstack + def test_lambda_sqs_integration_happy_path( + self, + aws_client, + monkeypatch, + mock_config_file, + ): + execution_arn = create_and_run_mock( + target_aws_client=aws_client, + monkeypatch=monkeypatch, + mock_config_file=mock_config_file, + mock_config=MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCK_CONFIG_FILE_LAMBDA_SQS_INTEGRATION + ), + state_machine_name="LambdaSQSIntegration", + definition_template=MockedTemplates.load_sfn_template( + MockedTemplates.LAMBDA_SQS_INTEGRATION + ), + execution_input="{}", + test_name="HappyPath", + ) + + execution_history = aws_client.stepfunctions.get_execution_history( + executionArn=execution_arn, includeExecutionData=True + ) + events = execution_history["events"] + + event_4 = events[4] + assert json.loads(event_4["taskSucceededEventDetails"]["output"]) == { + "StatusCode": 200, + "Payload": {"StatusCode": 200, "body": "Hello from Lambda!"}, + } + + event_last = events[-1] + assert event_last["type"] == "ExecutionSucceeded" + + @markers.aws.only_localstack + def test_lambda_sqs_integration_retry_path( + self, + aws_client, + monkeypatch, + mock_config_file, + ): + execution_arn = create_and_run_mock( + target_aws_client=aws_client, + monkeypatch=monkeypatch, + mock_config_file=mock_config_file, + mock_config=MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCK_CONFIG_FILE_LAMBDA_SQS_INTEGRATION + ), + state_machine_name="LambdaSQSIntegration", + definition_template=MockedTemplates.load_sfn_template( + MockedTemplates.LAMBDA_SQS_INTEGRATION + ), + execution_input="{}", + test_name="RetryPath", + ) + + execution_history = aws_client.stepfunctions.get_execution_history( + executionArn=execution_arn, includeExecutionData=True + ) + events = execution_history["events"] + + event_4 = events[4] + assert event_4["taskFailedEventDetails"] == { + "error": "Lambda.ResourceNotReadyException", + "cause": "Lambda resource is not ready.", + } + assert event_4["type"] == "TaskFailed" + + event_7 = events[7] + assert event_7["taskFailedEventDetails"] == { + "error": "Lambda.TimeoutException", + "cause": "Lambda timed out.", + } + assert event_7["type"] == "TaskFailed" + + event_10 = events[10] + assert event_10["taskFailedEventDetails"] == { + "error": "Lambda.TimeoutException", + "cause": "Lambda timed out.", + } + assert event_10["type"] == "TaskFailed" + + event_13 = events[13] + assert json.loads(event_13["taskSucceededEventDetails"]["output"]) == { + "StatusCode": 200, + "Payload": {"StatusCode": 200, "body": "Hello from Lambda!"}, + } + + event_last = events[-1] + assert event_last["type"] == "ExecutionSucceeded" + + @markers.aws.only_localstack + def test_lambda_sqs_integration_hybrid_path( + self, + aws_client, + sqs_create_queue, + monkeypatch, + mock_config_file, + ): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + definition_template = MockedTemplates.load_sfn_template( + MockedTemplates.LAMBDA_SQS_INTEGRATION + ) + definition_template["States"]["SQSState"]["Parameters"]["QueueUrl"] = queue_url + execution_arn = create_and_run_mock( + target_aws_client=aws_client, + monkeypatch=monkeypatch, + mock_config_file=mock_config_file, + mock_config=MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCK_CONFIG_FILE_LAMBDA_SQS_INTEGRATION + ), + state_machine_name="LambdaSQSIntegration", + definition_template=definition_template, + execution_input="{}", + test_name="HybridPath", + ) + + execution_history = aws_client.stepfunctions.get_execution_history( + executionArn=execution_arn, includeExecutionData=True + ) + events = execution_history["events"] + + event_4 = events[4] + assert json.loads(event_4["taskSucceededEventDetails"]["output"]) == { + "StatusCode": 200, + "Payload": {"StatusCode": 200, "body": "Hello from Lambda!"}, + } + + event_last = events[-1] + assert event_last["type"] == "ExecutionSucceeded" + receive_message_res = aws_client.sqs.receive_message( + QueueUrl=queue_url, MessageAttributeNames=["All"] + ) + assert len(receive_message_res["Messages"]) == 1 + + sqs_message = receive_message_res["Messages"][0] + print(sqs_message) + assert json.loads(sqs_message["Body"]) == { + "StatusCode": 200, + "Payload": {"StatusCode": 200, "body": "Hello from Lambda!"}, + } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py index de007aea22301..7273954337d03 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py @@ -1,7 +1,7 @@ import json import pytest -from localstack_snapshot.snapshots.transformer import JsonpathTransformer +from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack import config from localstack.testing.aws.util import is_aws_cloud @@ -12,8 +12,8 @@ create_state_machine_with_iam_role, ) from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( - MockedResponseLoader, +from tests.aws.services.stepfunctions.mocked_service_integrations.mocked_service_integrations import ( + MockedServiceIntegrationsLoader, ) from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate from tests.aws.services.stepfunctions.templates.callbacks.callback_templates import ( @@ -27,8 +27,11 @@ "$..SdkResponseMetadata", "$..ExecutedVersion", "$..RedriveCount", + "$..redriveCount", "$..RedriveStatus", + "$..redriveStatus", "$..RedriveStatusReason", + "$..redriveStatusReason", # In an effort to comply with SFN Local's lack of handling of sync operations, # we are unable to produce valid TaskSubmittedEventDetails output field, which # must include the provided mocked response in the output: @@ -42,11 +45,11 @@ class TestBaseScenarios: [ ( CallbackTemplates.SFN_START_EXECUTION_SYNC, - MockedResponseLoader.STATES_200_START_EXECUTION_SYNC, + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_STATES_200_START_EXECUTION_SYNC, ), ( CallbackTemplates.SFN_START_EXECUTION_SYNC2, - MockedResponseLoader.STATES_200_START_EXECUTION_SYNC2, + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_STATES_200_START_EXECUTION_SYNC2, ), ], ids=["SFN_SYNC", "SFN_SYNC2"], @@ -123,7 +126,7 @@ def test_sfn_start_execution_sync( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - mocked_response = MockedResponseLoader.load(mocked_response_filepath) + mocked_response = MockedServiceIntegrationsLoader.load(mocked_response_filepath) mock_config = { "StateMachines": { state_machine_name: { @@ -147,3 +150,155 @@ def test_sfn_start_execution_sync( state_machine_name, test_name, ) + + @markers.aws.validated + def test_sqs_wait_for_task_token( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + sqs_create_queue, + sqs_send_task_success_state_machine, + sfn_snapshot, + mock_config_file, + monkeypatch, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="task_token", + replace_reference=True, + ) + ) + + template = CallbackTemplates.load_sfn_template(CallbackTemplates.SQS_WAIT_FOR_TASK_TOKEN) + definition = json.dumps(template) + message = "string-literal" + + if is_aws_cloud(): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sqs_send_task_success_state_machine(queue_url) + + exec_input = json.dumps({"QueueUrl": queue_url, "Message": message}) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + task_success = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_CALLBACK_TASK_SUCCESS_STRING_LITERAL + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"SendMessageWithWait": "task_success"}} + } + }, + "MockedResponses": {"task_success": task_success}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + exec_input = json.dumps({"QueueUrl": "sqs_queue_url", "Message": message}) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Reason: skipping events validation because in mock‐failure mode the + # TaskSubmitted event is never emitted; this causes the events sequence + # to be shifted by one. Nevertheless, the evaluation of the state machine + # is still successful. + "$..events" + ] + ) + def test_sqs_wait_for_task_token_task_failure( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + sqs_create_queue, + sqs_send_task_failure_state_machine, + sfn_snapshot, + mock_config_file, + monkeypatch, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="task_token", + replace_reference=True, + ) + ) + + template = CallbackTemplates.load_sfn_template( + CallbackTemplates.SQS_WAIT_FOR_TASK_TOKEN_CATCH + ) + definition = json.dumps(template) + message = "string-literal" + + if is_aws_cloud(): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sqs_send_task_failure_state_machine(queue_url) + + exec_input = json.dumps({"QueueUrl": queue_url, "Message": message}) + execution_arn = create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + task_failure = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_CALLBACK_TASK_FAILURE + ) + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": {test_name: {"SendMessageWithWait": "task_failure"}} + } + }, + "MockedResponses": {"task_failure": task_failure}, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + exec_input = json.dumps({"QueueUrl": "sqs_queue_url", "Message": message}) + execution_arn = create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) + + describe_execution_response = aws_client.stepfunctions.describe_execution( + executionArn=execution_arn + ) + sfn_snapshot.match("describe_execution_response", describe_execution_response) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json index 39329e30b6dde..4628554c854af 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.snapshot.json @@ -422,5 +422,403 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sqs_wait_for_task_token": { + "recorded-date": "29-04-2025, 10:17:01", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn::iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Message": "string-literal", + "TaskToken": "" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": "\"string-literal\"", + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": "\"string-literal\"", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "\"string-literal\"", + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sqs_wait_for_task_token_task_failure": { + "recorded-date": "29-04-2025, 11:15:14", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn::iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Context": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "TaskToken": "" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": "x-amzn-RequestId", + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "x-amzn-RequestId" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskFailedEventDetails": { + "cause": "Failure cause", + "error": "Failure error", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal", + "states_all_error": { + "Error": "Failure error", + "Cause": "Failure cause" + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": 8, + "previousEventId": 7, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal", + "states_all_error": { + "Error": "Failure error", + "Cause": "Failure cause" + } + }, + "inputDetails": { + "truncated": false + }, + "name": "CaughtStatesALL" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 9, + "previousEventId": 8, + "stateExitedEventDetails": { + "name": "CaughtStatesALL", + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal", + "states_all_error": { + "Error": "Failure error", + "Cause": "Failure cause" + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal", + "states_all_error": { + "Error": "Failure error", + "Cause": "Failure cause" + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 10, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_execution_response": { + "executionArn": "arn::states::111111111111:execution::", + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal" + }, + "inputDetails": { + "included": true + }, + "name": "", + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "string-literal", + "states_all_error": { + "Error": "Failure error", + "Cause": "Failure cause" + } + }, + "outputDetails": { + "included": true + }, + "redriveCount": 0, + "redriveStatus": "NOT_REDRIVABLE", + "redriveStatusReason": "Execution is SUCCEEDED and cannot be redriven", + "startDate": "datetime", + "stateMachineArn": "arn::states::111111111111:stateMachine:", + "status": "SUCCEEDED", + "stopDate": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json index 5bfb11ec0f06a..1151f58cdcd1e 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.validation.json @@ -4,5 +4,11 @@ }, "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sfn_start_execution_sync[SFN_SYNC]": { "last_validated_date": "2025-04-24T10:05:48+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sqs_wait_for_task_token": { + "last_validated_date": "2025-04-29T10:17:01+00:00" + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_callbacks.py::TestBaseScenarios::test_sqs_wait_for_task_token_task_failure": { + "last_validated_date": "2025-04-29T11:15:14+00:00" } } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py index 6267001f37700..7c30e0d513801 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py @@ -9,13 +9,14 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.testing.pytest.stepfunctions.utils import ( + SfnNoneRecursiveParallelTransformer, await_execution_terminated, create_and_record_execution, create_and_record_mocked_execution, ) from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( - MockedResponseLoader, +from tests.aws.services.stepfunctions.mocked_service_integrations.mocked_service_integrations import ( + MockedServiceIntegrationsLoader, ) from tests.aws.services.stepfunctions.templates.scenarios.scenarios_templates import ( ScenariosTemplate, @@ -64,8 +65,8 @@ def test_lambda_invoke( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - lambda_200_string_body = MockedResponseLoader.load( - MockedResponseLoader.LAMBDA_200_STRING_BODY + lambda_200_string_body = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_LAMBDA_200_STRING_BODY ) mock_config = { "StateMachines": { @@ -114,8 +115,8 @@ def test_lambda_invoke_retries( state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - lambda_not_ready_timeout_200_string_body = MockedResponseLoader.load( - MockedResponseLoader.LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY + lambda_not_ready_timeout_200_string_body = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_LAMBDA_NOT_READY_TIMEOUT_200_STRING_BODY ) mock_config = { "StateMachines": { @@ -208,8 +209,8 @@ def test_lambda_service_invoke( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - lambda_200_string_body = MockedResponseLoader.load( - MockedResponseLoader.LAMBDA_200_STRING_BODY + lambda_200_string_body = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_LAMBDA_200_STRING_BODY ) mock_config = { "StateMachines": { @@ -268,8 +269,8 @@ def test_sqs_send_message( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - sqs_200_send_message = MockedResponseLoader.load( - MockedResponseLoader.SQS_200_SEND_MESSAGE + sqs_200_send_message = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_SQS_200_SEND_MESSAGE ) mock_config = { "StateMachines": { @@ -324,7 +325,9 @@ def test_sns_publish_base( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - sns_200_publish = MockedResponseLoader.load(MockedResponseLoader.SNS_200_PUBLISH) + sns_200_publish = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_SNS_200_PUBLISH + ) mock_config = { "StateMachines": { state_machine_name: {"TestCases": {test_name: {"Publish": "sns_200_publish"}}} @@ -386,8 +389,8 @@ def test_events_put_events( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - events_200_put_events = MockedResponseLoader.load( - MockedResponseLoader.EVENTS_200_PUT_EVENTS + events_200_put_events = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_EVENTS_200_PUT_EVENTS ) mock_config = { "StateMachines": { @@ -450,11 +453,11 @@ def test_dynamodb_put_get_item( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - dynamodb_200_put_item = MockedResponseLoader.load( - MockedResponseLoader.DYNAMODB_200_PUT_ITEM + dynamodb_200_put_item = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_DYNAMODB_200_PUT_ITEM ) - dynamodb_200_get_item = MockedResponseLoader.load( - MockedResponseLoader.DYNAMODB_200_GET_ITEM + dynamodb_200_get_item = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_DYNAMODB_200_GET_ITEM ) mock_config = { "StateMachines": { @@ -532,8 +535,8 @@ def test_map_state_lambda( else: state_machine_name = f"mocked_state_machine_{short_uid()}" test_name = "TestCaseName" - lambda_200_string_body = MockedResponseLoader.load( - MockedResponseLoader.LAMBDA_200_STRING_BODY + lambda_200_string_body = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_LAMBDA_200_STRING_BODY ) mock_config = { "StateMachines": { @@ -558,3 +561,100 @@ def test_map_state_lambda( state_machine_name, test_name, ) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=["$..stateExitedEventDetails.output", "$..executionSucceededEventDetails.output"] + ) + def test_parallel_state_lambda( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + create_lambda_function, + sfn_snapshot, + monkeypatch, + mock_config_file, + ): + sfn_snapshot.add_transformer(SfnNoneRecursiveParallelTransformer()) + template = ScenariosTemplate.load_sfn_template( + ScenariosTemplate.PARALLEL_STATE_SERVICE_LAMBDA + ) + definition = json.dumps(template) + + function_name_branch1 = f"lambda_branch1_{short_uid()}" + sfn_snapshot.add_transformer( + RegexTransformer(function_name_branch1, "function_name_branch1") + ) + function_name_branch2 = f"lambda_branch2_{short_uid()}" + sfn_snapshot.add_transformer( + RegexTransformer(function_name_branch2, "function_name_branch2") + ) + + exec_input = json.dumps( + { + "FunctionNameBranch1": function_name_branch1, + "FunctionNameBranch2": function_name_branch2, + "Payload": ["string-literal"], + } + ) + + if is_aws_cloud(): + create_lambda_function( + func_name=function_name_branch1, + handler_file=ServicesTemplates.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + create_lambda_function( + func_name=function_name_branch2, + handler_file=ServicesTemplates.LAMBDA_RETURN_DECORATED_INPUT, + runtime=Runtime.python3_12, + ) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + else: + state_machine_name = f"mocked_state_machine_{short_uid()}" + test_name = "TestCaseName" + mock_config = { + "StateMachines": { + state_machine_name: { + "TestCases": { + test_name: { + "Branch1": "MockedBranch1", + "Branch2": "MockedBranch2", + } + } + } + }, + "MockedResponses": { + "MockedBranch1": { + "0": {"Return": {"StatusCode": 200, "Payload": ["string-literal"]}} + }, + "MockedBranch2": { + "0": { + "Return": { + "StatusCode": 200, + "Payload": "input-event-['string-literal']", + } + } + }, + }, + } + mock_config_file_path = mock_config_file(mock_config) + monkeypatch.setattr(config, "SFN_MOCK_CONFIG", mock_config_file_path) + create_and_record_mocked_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + state_machine_name, + test_name, + ) diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json index 613e3a8d146f7..825c405214dcb 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.snapshot.json @@ -2063,5 +2063,417 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_lambda": { + "recorded-date": "28-04-2025, 12:36:06", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "FunctionNameBranch1": "function_name_branch1", + "FunctionNameBranch2": "function_name_branch2", + "Payload": [ + "string-literal" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "FunctionNameBranch1": "function_name_branch1", + "FunctionNameBranch2": "function_name_branch2", + "Payload": [ + "string-literal" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "ParallelState" + }, + "timestamp": "timestamp", + "type": "ParallelStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ParallelStateStarted" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "FunctionNameBranch1": "function_name_branch1", + "FunctionNameBranch2": "function_name_branch2", + "Payload": [ + "string-literal" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "Branch1" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "FunctionNameBranch1": "function_name_branch1", + "FunctionNameBranch2": "function_name_branch2", + "Payload": [ + "string-literal" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "Branch2" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateExitedEventDetails": { + "name": "Branch1", + "output": { + "ExecutedVersion": "$LATEST", + "Payload": [ + "string-literal" + ], + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "18" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "18", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateExitedEventDetails": { + "name": "Branch2", + "output": { + "ExecutedVersion": "$LATEST", + "Payload": "input-event-['string-literal']", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "32" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "32", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskScheduledEventDetails": { + "parameters": { + "FunctionName": "function_name_branch1", + "Payload": [ + "string-literal" + ] + }, + "region": "", + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskScheduledEventDetails": { + "parameters": { + "FunctionName": "function_name_branch2", + "Payload": [ + "string-literal" + ] + }, + "region": "", + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskStartedEventDetails": { + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskStartedEventDetails": { + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskSucceededEventDetails": { + "output": { + "ExecutedVersion": "$LATEST", + "Payload": "input-event-['string-literal']", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "32" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "32", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + }, + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "taskSucceededEventDetails": { + "output": { + "ExecutedVersion": "$LATEST", + "Payload": [ + "string-literal" + ], + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": "x-amzn-RequestId", + "Content-Length": [ + "18" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "18", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "x-amzn-RequestId", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "RequestId" + }, + "StatusCode": 200 + }, + "outputDetails": { + "truncated": false + }, + "resource": "invoke", + "resourceType": "lambda" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 15, + "previousEventId": 13, + "stateExitedEventDetails": { + "name": "ParallelState", + "output": "[{\"ExecutedVersion\":\"$LATEST\",\"Payload\":[\"string-literal\"],\"SdkHttpMetadata\":{\"AllHttpHeaders\":{\"X-Amz-Executed-Version\":[\"$LATEST\"],\"x-amzn-Remapped-Content-Length\":[\"0\"],\"Connection\":[\"keep-alive\"],\"x-amzn-RequestId\":[\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\"],\"Content-Length\":[\"18\"],\"Date\":[\"Mon, 28 Apr 2025 12:36:05 GMT\"],\"X-Amzn-Trace-Id\":[\"Root=1-680f7635-0182b6d14f9778376d202e25;Parent=7880c132e0e2009b;Sampled=0;Lineage=1:816810b0:0\"],\"Content-Type\":[\"application/json\"]},\"HttpHeaders\":{\"Connection\":\"keep-alive\",\"Content-Length\":\"18\",\"Content-Type\":\"application/json\",\"Date\":\"Mon, 28 Apr 2025 12:36:05 GMT\",\"X-Amz-Executed-Version\":\"$LATEST\",\"x-amzn-Remapped-Content-Length\":\"0\",\"x-amzn-RequestId\":\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\",\"X-Amzn-Trace-Id\":\"Root=1-680f7635-0182b6d14f9778376d202e25;Parent=7880c132e0e2009b;Sampled=0;Lineage=1:816810b0:0\"},\"HttpStatusCode\":200},\"SdkResponseMetadata\":{\"RequestId\":\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\"},\"StatusCode\":200},{\"ExecutedVersion\":\"$LATEST\",\"Payload\":\"input-event-['string-literal']\",\"SdkHttpMetadata\":{\"AllHttpHeaders\":{\"X-Amz-Executed-Version\":[\"$LATEST\"],\"x-amzn-Remapped-Content-Length\":[\"0\"],\"Connection\":[\"keep-alive\"],\"x-amzn-RequestId\":[\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\"],\"Content-Length\":[\"32\"],\"Date\":[\"Mon, 28 Apr 2025 12:36:05 GMT\"],\"X-Amzn-Trace-Id\":[\"Root=1-680f7635-5bd01853792e73951459090c;Parent=56aa9955dca169e9;Sampled=0;Lineage=1:786b2a01:0\"],\"Content-Type\":[\"application/json\"]},\"HttpHeaders\":{\"Connection\":\"keep-alive\",\"Content-Length\":\"32\",\"Content-Type\":\"application/json\",\"Date\":\"Mon, 28 Apr 2025 12:36:05 GMT\",\"X-Amz-Executed-Version\":\"$LATEST\",\"x-amzn-Remapped-Content-Length\":\"0\",\"x-amzn-RequestId\":\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\",\"X-Amzn-Trace-Id\":\"Root=1-680f7635-5bd01853792e73951459090c;Parent=56aa9955dca169e9;Sampled=0;Lineage=1:786b2a01:0\"},\"HttpStatusCode\":200},\"SdkResponseMetadata\":{\"RequestId\":\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\"},\"StatusCode\":200}]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ParallelStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "[{\"ExecutedVersion\":\"$LATEST\",\"Payload\":[\"string-literal\"],\"SdkHttpMetadata\":{\"AllHttpHeaders\":{\"X-Amz-Executed-Version\":[\"$LATEST\"],\"x-amzn-Remapped-Content-Length\":[\"0\"],\"Connection\":[\"keep-alive\"],\"x-amzn-RequestId\":[\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\"],\"Content-Length\":[\"18\"],\"Date\":[\"Mon, 28 Apr 2025 12:36:05 GMT\"],\"X-Amzn-Trace-Id\":[\"Root=1-680f7635-0182b6d14f9778376d202e25;Parent=7880c132e0e2009b;Sampled=0;Lineage=1:816810b0:0\"],\"Content-Type\":[\"application/json\"]},\"HttpHeaders\":{\"Connection\":\"keep-alive\",\"Content-Length\":\"18\",\"Content-Type\":\"application/json\",\"Date\":\"Mon, 28 Apr 2025 12:36:05 GMT\",\"X-Amz-Executed-Version\":\"$LATEST\",\"x-amzn-Remapped-Content-Length\":\"0\",\"x-amzn-RequestId\":\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\",\"X-Amzn-Trace-Id\":\"Root=1-680f7635-0182b6d14f9778376d202e25;Parent=7880c132e0e2009b;Sampled=0;Lineage=1:816810b0:0\"},\"HttpStatusCode\":200},\"SdkResponseMetadata\":{\"RequestId\":\"c455c86f-6cb5-4d5d-8a71-4f0a98b33059\"},\"StatusCode\":200},{\"ExecutedVersion\":\"$LATEST\",\"Payload\":\"input-event-['string-literal']\",\"SdkHttpMetadata\":{\"AllHttpHeaders\":{\"X-Amz-Executed-Version\":[\"$LATEST\"],\"x-amzn-Remapped-Content-Length\":[\"0\"],\"Connection\":[\"keep-alive\"],\"x-amzn-RequestId\":[\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\"],\"Content-Length\":[\"32\"],\"Date\":[\"Mon, 28 Apr 2025 12:36:05 GMT\"],\"X-Amzn-Trace-Id\":[\"Root=1-680f7635-5bd01853792e73951459090c;Parent=56aa9955dca169e9;Sampled=0;Lineage=1:786b2a01:0\"],\"Content-Type\":[\"application/json\"]},\"HttpHeaders\":{\"Connection\":\"keep-alive\",\"Content-Length\":\"32\",\"Content-Type\":\"application/json\",\"Date\":\"Mon, 28 Apr 2025 12:36:05 GMT\",\"X-Amz-Executed-Version\":\"$LATEST\",\"x-amzn-Remapped-Content-Length\":\"0\",\"x-amzn-RequestId\":\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\",\"X-Amzn-Trace-Id\":\"Root=1-680f7635-5bd01853792e73951459090c;Parent=56aa9955dca169e9;Sampled=0;Lineage=1:786b2a01:0\"},\"HttpStatusCode\":200},\"SdkResponseMetadata\":{\"RequestId\":\"bee07bb2-41e2-4c9f-9f6f-1a70696cc112\"},\"StatusCode\":200}]", + "outputDetails": { + "truncated": false + } + }, + "id": 16, + "previousEventId": 15, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json index fff3ec14317af..11b63a4402426 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.validation.json @@ -14,6 +14,9 @@ "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_map_state_lambda": { "last_validated_date": "2025-04-24T11:11:05+00:00" }, + "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_lambda": { + "last_validated_date": "2025-04-28T12:36:06+00:00" + }, "tests/aws/services/stepfunctions/v2/mocking/test_base_scenarios.py::TestBaseScenarios::test_sns_publish_base": { "last_validated_date": "2025-04-23T13:52:23+00:00" }, diff --git a/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py index f460e96b107e6..931d66512936e 100644 --- a/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py +++ b/tests/aws/services/stepfunctions/v2/mocking/test_mock_config_file.py @@ -4,8 +4,8 @@ load_mock_test_case_for, ) from localstack.testing.pytest import markers -from tests.aws.services.stepfunctions.mocked_responses.mocked_response_loader import ( - MockedResponseLoader, +from tests.aws.services.stepfunctions.mocked_service_integrations.mocked_service_integrations import ( + MockedServiceIntegrationsLoader, ) @@ -19,8 +19,8 @@ def test_is_mock_config_flag_detected_unset(self, mock_config_file): @markers.aws.only_localstack def test_is_mock_config_flag_detected_set(self, mock_config_file, monkeypatch): - lambda_200_string_body = MockedResponseLoader.load( - MockedResponseLoader.LAMBDA_200_STRING_BODY + lambda_200_string_body = MockedServiceIntegrationsLoader.load( + MockedServiceIntegrationsLoader.MOCKED_RESPONSE_LAMBDA_200_STRING_BODY ) # TODO: add typing for MockConfigFile.json components mock_config = { From b9b868c886775a893a474f92d7967a792d0f4ec7 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 1 May 2025 00:46:53 +0200 Subject: [PATCH 091/108] refactor Counter usage in APIGW (#12569) --- .../localstack/services/apigateway/analytics.py | 5 +++++ .../apigateway/next_gen/execute_api/handlers/__init__.py | 4 +++- .../apigateway/next_gen/execute_api/handlers/analytics.py | 8 +++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 localstack-core/localstack/services/apigateway/analytics.py diff --git a/localstack-core/localstack/services/apigateway/analytics.py b/localstack-core/localstack/services/apigateway/analytics.py new file mode 100644 index 0000000000000..13bd7109358ce --- /dev/null +++ b/localstack-core/localstack/services/apigateway/analytics.py @@ -0,0 +1,5 @@ +from localstack.utils.analytics.metrics import Counter + +invocation_counter = Counter( + namespace="apigateway", name="rest_api_execute", labels=["invocation_type"] +) diff --git a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/__init__.py b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/__init__.py index 5e4a8a27f97b4..e9e1dcb618166 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/__init__.py +++ b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/__init__.py @@ -1,5 +1,7 @@ from rolo.gateway import CompositeHandler +from localstack.services.apigateway.analytics import invocation_counter + from .analytics import IntegrationUsageCounter from .api_key_validation import ApiKeyValidationHandler from .gateway_exception import GatewayExceptionHandler @@ -24,4 +26,4 @@ gateway_exception_handler = GatewayExceptionHandler() api_key_validation_handler = ApiKeyValidationHandler() response_enricher = InvocationResponseEnricher() -usage_counter = IntegrationUsageCounter() +usage_counter = IntegrationUsageCounter(counter=invocation_counter) diff --git a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/analytics.py b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/analytics.py index 82ba2b7d2593c..7c6525eb0e7e1 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/analytics.py +++ b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/analytics.py @@ -1,7 +1,7 @@ import logging from localstack.http import Response -from localstack.utils.analytics.metrics import Counter, LabeledCounterMetric +from localstack.utils.analytics.metrics import LabeledCounterMetric from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain from ..context import RestApiInvocationContext @@ -12,10 +12,8 @@ class IntegrationUsageCounter(RestApiGatewayHandler): counter: LabeledCounterMetric - def __init__(self, counter: LabeledCounterMetric = None): - self.counter = counter or Counter( - namespace="apigateway", name="rest_api_execute", labels=["invocation_type"] - ) + def __init__(self, counter: LabeledCounterMetric): + self.counter = counter def __call__( self, From 7a29f279cead7d61b1ef94736c10b122a541084e Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Thu, 1 May 2025 12:15:50 +0530 Subject: [PATCH 092/108] Bump moto-ext to 5.1.4.post1 (#12563) --- pyproject.toml | 2 +- requirements-dev.txt | 2 +- requirements-runtime.txt | 2 +- requirements-test.txt | 2 +- requirements-typehint.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3dbdac8b03d07..f6f8acab0c02a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.1.3.post1", + "moto-ext[all]==5.1.4.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index e1e13d2578aa0..5996d434a48c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -250,7 +250,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.7.0 # via openapi-core -moto-ext==5.1.3.post1 +moto-ext==5.1.4.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 75f8e05e732e4..ae8018c286d54 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -188,7 +188,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.7.0 # via openapi-core -moto-ext==5.1.3.post1 +moto-ext==5.1.4.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy diff --git a/requirements-test.txt b/requirements-test.txt index a55221103742a..9fa7f02cddd32 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -234,7 +234,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.7.0 # via openapi-core -moto-ext==5.1.3.post1 +moto-ext==5.1.4.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-typehint.txt b/requirements-typehint.txt index ae10a3ed31d61..a9583aa8836c4 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -254,7 +254,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.7.0 # via openapi-core -moto-ext==5.1.3.post1 +moto-ext==5.1.4.post1 # via localstack-core mpmath==1.3.0 # via sympy From 940aa0a23638ff91b0af9a6adec404a0f4b986c7 Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Fri, 2 May 2025 16:11:00 +0200 Subject: [PATCH 093/108] Migrate full-run integration tests from CircleCI to GH Actions except docker push and push of all artifacts (#12545) --- .../action.yml | 29 + .github/actions/setup-tests-env/action.yml | 22 + .github/workflows/aws-tests.yml | 676 ++++++++++++++++++ Makefile | 4 +- 4 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 .github/actions/load-localstack-docker-from-artifacts/action.yml create mode 100644 .github/actions/setup-tests-env/action.yml create mode 100644 .github/workflows/aws-tests.yml diff --git a/.github/actions/load-localstack-docker-from-artifacts/action.yml b/.github/actions/load-localstack-docker-from-artifacts/action.yml new file mode 100644 index 0000000000000..e1928cd0433b3 --- /dev/null +++ b/.github/actions/load-localstack-docker-from-artifacts/action.yml @@ -0,0 +1,29 @@ +name: 'Load Localstack Docker image' +description: 'Composite action that loads a LocalStack Docker image from a tar archive stored in GitHub Workflow Artifacts into the local Docker image cache' +inputs: + platform: + required: false + description: Target architecture for running the Docker image + default: "amd64" +runs: + using: "composite" + steps: + - name: Download Docker Image + uses: actions/download-artifact@v4 + with: + name: localstack-docker-image-${{ inputs.platform }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: '.python-version' + cache: 'pip' + cache-dependency-path: 'requirements-dev.txt' + + - name: Install docker helper dependencies + shell: bash + run: pip install --upgrade setuptools setuptools_scm + + - name: Load Docker Image + shell: bash + run: bin/docker-helper.sh load diff --git a/.github/actions/setup-tests-env/action.yml b/.github/actions/setup-tests-env/action.yml new file mode 100644 index 0000000000000..bb8c467628165 --- /dev/null +++ b/.github/actions/setup-tests-env/action.yml @@ -0,0 +1,22 @@ +name: 'Setup Test Environment' +description: 'Composite action which combines all steps necessary to setup the runner for test execution' +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: '.python-version' + cache: 'pip' + cache-dependency-path: 'requirements-dev.txt' + + - name: Install Community Dependencies + shell: bash + run: make install-dev + + - name: Setup environment + shell: bash + run: | + make install + mkdir -p target/reports + mkdir -p target/coverage diff --git a/.github/workflows/aws-tests.yml b/.github/workflows/aws-tests.yml new file mode 100644 index 0000000000000..f708a23799600 --- /dev/null +++ b/.github/workflows/aws-tests.yml @@ -0,0 +1,676 @@ +name: AWS Integration Tests + +on: + workflow_dispatch: + inputs: + disableCaching: + description: 'Disable Caching' + required: false + type: boolean + default: false + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING + disableTestSelection: + description: 'Disable Test Selection' + required: false + type: boolean + default: false + randomize-aws-credentials: + description: "Randomize AWS credentials" + default: false + required: false + type: boolean + onlyAcceptanceTests: + description: "Run only acceptance tests" + default: false + required: false + type: boolean + workflow_call: + inputs: + disableCaching: + description: 'Disable Caching' + required: false + type: boolean + default: false + PYTEST_LOGLEVEL: + type: string + required: false + description: Loglevel for PyTest + default: WARNING + disableTestSelection: + description: 'Disable Test Selection' + required: false + type: boolean + default: false + randomize-aws-credentials: + description: "Randomize AWS credentials" + default: false + required: false + type: boolean + onlyAcceptanceTests: + description: "Run only acceptance tests" + default: false + required: false + type: boolean + +env: + PYTEST_LOGLEVEL: ${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }} + IMAGE_NAME: "localstack/localstack" + TINYBIRD_DATASOURCE: "community_tests_integration" + TESTSELECTION_PYTEST_ARGS: "${{ !inputs.disableTestSelection && '--path-filter=dist/testselection/test-selection.txt ' || '' }}" + +jobs: + build: + name: "Build Docker Image (${{ contains(matrix.runner, 'arm') && 'ARM64' || 'AMD64' }})" + needs: + - test-preflight + strategy: + matrix: + runner: + - ubuntu-latest + - ubuntu-24.04-arm + exclude: + # skip the ARM integration tests in case we are not on the master and not on the upgrade-dependencies branch and forceARMTests is not set to true + - runner: ${{ (github.ref != 'refs/heads/master' && github.ref != 'upgrade-dependencies' && inputs.forceARMTests == false) && 'ubuntu-24.04-arm' || ''}} + fail-fast: false + runs-on: ${{ matrix.runner }} + steps: + - name: Determine Runner Architecture + shell: bash + run: echo "PLATFORM=${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + path: localstack + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Build Image + uses: localstack/localstack/.github/actions/build-image@master + with: + disableCaching: ${{ inputs.disableCaching == true && 'true' || 'false' }} + dockerhubPullUsername: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + dockerhubPullToken: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + + - name: Restore Lambda common runtime packages + id: cached-lambda-common-restore + if: inputs.disableCaching != true + uses: actions/cache/restore@v4 + with: + path: localstack/tests/aws/services/lambda_/functions/common + key: common-it-${{ runner.os }}-${{ runner.arch }}-lambda-common-${{ hashFiles('localstack/tests/aws/services/lambda_/functions/common/**/src/*', 'localstack/tests/aws/services/lambda_/functions/common/**/Makefile') }} + + - name: Prebuild lambda common packages + run: ./localstack/scripts/build_common_test_functions.sh `pwd`/localstack/tests/aws/services/lambda_/functions/common + + - name: Save Lambda common runtime packages + if: inputs.disableCaching != true + uses: actions/cache/save@v4 + with: + path: localstack/tests/aws/services/lambda_/functions/common + key: ${{ steps.cached-lambda-common-restore.outputs.cache-primary-key }} + + - name: Archive Lambda common packages + uses: actions/upload-artifact@v4 + with: + name: lambda-common-${{ env.PLATFORM }} + path: | + localstack/tests/aws/services/lambda_/functions/common + retention-days: 1 + + + test-preflight: + name: "Preflight & Unit-Tests" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Linting + run: make lint + + - name: Check AWS compatibility markers + run: make check-aws-markers + + - name: Determine Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + run: | + source .venv/bin/activate + if [ -z "${{ github.event.pull_request.base.sha }}" ]; then + echo "Do test selection based on branch name" + else + echo "Do test selection based on Pull Request event" + SCRIPT_OPTS="--base-commit-sha ${{ github.event.pull_request.base.sha }} --head-commit-sha ${{ github.event.pull_request.head.sha }}" + fi + source .venv/bin/activate + python -m localstack.testing.testselection.scripts.generate_test_selection $(pwd) dist/testselection/test-selection.txt $SCRIPT_OPTS || (mkdir -p dist/testselection && echo "SENTINEL_ALL_TESTS" >> dist/testselection/test-selection.txt) + echo "Test selection:" + cat dist/testselection/test-selection.txt + + - name: Archive Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + uses: actions/upload-artifact@v4 + with: + name: test-selection + path: | + dist/testselection/test-selection.txt + retention-days: 1 + + - name: Run Unit Tests + timeout-minutes: 8 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBUG: 1 + TEST_PATH: "tests/unit" + JUNIT_REPORTS_FILE: "pytest-junit-unit.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} -o junit_suite_name=unit-tests" + COVERAGE_FILE: ".coverage.unit" + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }}-unit + CI_JOB_ID: ${{ github.job }}-unit + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-preflight + include-hidden-files: true + path: | + pytest-junit-unit.xml + .coverage.unit + retention-days: 30 + + test-integration: + name: "Integration Tests (${{ contains(matrix.runner, 'arm') && 'ARM64' || 'AMD64' }} - ${{ matrix.group }})" + if: ${{ !inputs.onlyAcceptanceTests }} + needs: + - build + - test-preflight + strategy: + matrix: + group: [ 1, 2, 3, 4 ] + runner: + - ubuntu-latest + - ubuntu-24.04-arm + exclude: + # skip the ARM integration tests in case we are not on the master and not on the upgrade-dependencies branch and forceARMTests is not set to true + - runner: ${{ (github.ref != 'refs/heads/master' && github.ref != 'upgrade-dependencies' && inputs.forceARMTests == false) && 'ubuntu-24.04-arm' || ''}} + fail-fast: false + runs-on: ${{ matrix.runner }} + env: + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }}-${{ contains(matrix.runner, 'arm') && 'arm' || 'amd' }} + CI_JOB_ID: ${{ github.job }}-${{ contains(matrix.runner, 'arm') && 'arm' || 'amd' }} + steps: + - name: Determine Runner Architecture + shell: bash + run: echo "PLATFORM=${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }}" >> $GITHUB_ENV + + - name: Login to Docker Hub + # login to DockerHub to avoid rate limiting issues on custom runners + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + password: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + + - name: Set environment + if: ${{ inputs.testEnvironmentVariables != ''}} + shell: bash + run: | + echo "${{ inputs.testEnvironmentVariables }}" | sed "s/;/\n/" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Download Lambda Common packages + uses: actions/download-artifact@v4 + with: + name: lambda-common-${{ env.PLATFORM }} + path: | + tests/aws/services/lambda_/functions/common + + - name: Load Localstack Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: "${{ env.PLATFORM }}" + + - name: Download Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + uses: actions/download-artifact@v4 + with: + name: test-selection + path: dist/testselection/ + + - name: Run Integration Tests + timeout-minutes: 120 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}${{ env.TESTSELECTION_PYTEST_ARGS }} --splits 4 --group ${{ matrix.group }} --store-durations --clean-durations --ignore=tests/unit/ --ignore=tests/bootstrap" + COVERAGE_FILE: "target/.coverage.integration-${{ env.PLATFORM }}-${{ matrix.group }}" + JUNIT_REPORTS_FILE: "target/pytest-junit-integration-${{ env.PLATFORM }}-${{ matrix.group }}.xml" + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + # increase Docker SDK timeout to avoid timeouts on BuildJet runners - https://github.com/docker/docker-py/issues/2266 + DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS: 300 + run: make docker-run-tests + + - name: Archive Test Durations + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: pytest-split-durations-${{ env.PLATFORM }}-${{ matrix.group }} + path: .test_durations + include-hidden-files: true + retention-days: 5 + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-integration-${{ env.PLATFORM }}-${{ matrix.group }} + include-hidden-files: true + path: | + target/pytest-junit-integration-${{ env.PLATFORM }}-${{ matrix.group }}.xml + target/.coverage.integration-${{ env.PLATFORM }}-${{ matrix.group }} + retention-days: 30 + + test-bootstrap: + name: Test Bootstrap + if: ${{ !inputs.onlyAcceptanceTests }} + runs-on: ubuntu-latest + needs: + - test-preflight + - build + timeout-minutes: 60 + env: + PLATFORM: 'amd64' + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }} + CI_JOB_ID: ${{ github.job }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Load Localstack Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: "${{ env.PLATFORM }}" + + - name: Run Bootstrap Tests + timeout-minutes: 30 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEST_PATH: "tests/bootstrap" + COVERAGE_FILE: ".coverage.bootstrap" + JUNIT_REPORTS_FILE: "pytest-junit-bootstrap.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} -o junit_suite_name=bootstrap-tests" + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-bootstrap + include-hidden-files: true + path: | + pytest-junit-bootstrap.xml + .coverage.bootstrap + retention-days: 30 + + test-acceptance: + name: "Acceptance Tests (${{ contains(matrix.runner, 'arm') && 'ARM64' || 'AMD64' }}" + needs: + - build + strategy: + matrix: + runner: + - ubuntu-latest + - ubuntu-24.04-arm + exclude: + # skip the ARM integration tests in case we are not on the master and not on the upgrade-dependencies branch and forceARMTests is not set to true + - runner: ${{ (github.ref != 'refs/heads/master' && github.ref != 'upgrade-dependencies' && inputs.forceARMTests == false) && 'ubuntu-24.04-arm' || ''}} + fail-fast: false + runs-on: ${{ matrix.runner }} + env: + # Acceptance tests are executed for all test cases, without any test selection + TESTSELECTION_PYTEST_ARGS: "" + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }}-${{ contains(matrix.runner, 'arm') && 'arm' || 'amd' }} + CI_JOB_ID: ${{ github.job }}-${{ contains(matrix.runner, 'arm') && 'arm' || 'amd' }} + steps: + - name: Determine Runner Architecture + shell: bash + run: echo "PLATFORM=${{ (runner.arch == 'X64' && 'amd64') || (runner.arch == 'ARM64' && 'arm64') || '' }}" >> $GITHUB_ENV + + - name: Login to Docker Hub + # login to DockerHub to avoid rate limiting issues on custom runners + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + password: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + + - name: Set environment + if: ${{ inputs.testEnvironmentVariables != ''}} + shell: bash + run: | + echo "${{ inputs.testEnvironmentVariables }}" | sed "s/;/\n/" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Load Localstack Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: "${{ env.PLATFORM }}" + + - name: Run Acceptance Tests + timeout-minutes: 120 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBUG: 1 + LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC: 1 + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}${{ env.TESTSELECTION_PYTEST_ARGS }} --reruns 3 -m acceptance_test -o junit_suite_name='acceptance_test'" + COVERAGE_FILE: "target/.coverage.acceptance-${{ env.PLATFORM }}" + JUNIT_REPORTS_FILE: "target/pytest-junit-acceptance-${{ env.PLATFORM }}.xml" + TEST_PATH: "tests/aws/" + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + run: make docker-run-tests + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-acceptance-${{ env.PLATFORM }} + include-hidden-files: true + path: | + target/pytest-junit-acceptance-${{ env.PLATFORM }}.xml + target/.coverage.acceptance-${{ env.PLATFORM }} + retention-days: 30 + + test-cloudwatch-v1: + name: Test CloudWatch V1 + if: ${{ !inputs.onlyAcceptanceTests }} + runs-on: ubuntu-latest + needs: + - test-preflight + - build + timeout-minutes: 60 + env: + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }} + CI_JOB_ID: ${{ github.job }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Run Cloudwatch v1 Provider Tests + timeout-minutes: 30 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBUG: 1 + COVERAGE_FILE: ".coverage.cloudwatch_v1" + TEST_PATH: "tests/aws/services/cloudwatch/" + JUNIT_REPORTS_FILE: "pytest-junit-cloudwatch-v1.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} --reruns 3 -o junit_suite_name=cloudwatch_v1" + PROVIDER_OVERRIDE_CLOUDWATCH: "v1" + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-cloudwatch-v1 + include-hidden-files: true + path: | + pytest-junit-cloudwatch-v1.xml + .coverage.cloudwatch_v1 + retention-days: 30 + + test-ddb-v2: + name: Test DynamoDB(Streams) v2 + if: ${{ !inputs.onlyAcceptanceTests }} + runs-on: ubuntu-latest + needs: + - test-preflight + - build + timeout-minutes: 60 + env: + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }} + CI_JOB_ID: ${{ github.job }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Download Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + uses: actions/download-artifact@v4 + with: + name: test-selection + path: dist/testselection/ + + - name: Run DynamoDB(Streams) v2 Provider Tests + timeout-minutes: 30 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERAGE_FILE: ".coverage.dynamodb_v2" + TEST_PATH: "tests/aws/services/dynamodb/ tests/aws/services/dynamodbstreams/ tests/aws/services/lambda_/event_source_mapping/test_lambda_integration_dynamodbstreams.py" + JUNIT_REPORTS_FILE: "pytest-junit-dynamodb-v2.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} --reruns 3 -o junit_suite_name=dynamodb_v2" + PROVIDER_OVERRIDE_DYNAMODB: "v2" + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-dynamodb-v2 + include-hidden-files: true + path: | + pytest-junit-dynamodb-v2.xml + .coverage.dynamodb_v2 + retention-days: 30 + + test-events-v1: + name: Test EventBridge v1 + if: ${{ !inputs.onlyAcceptanceTests }} + runs-on: ubuntu-latest + needs: + - test-preflight + - build + timeout-minutes: 60 + env: + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }} + CI_JOB_ID: ${{ github.job }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Download Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + uses: actions/download-artifact@v4 + with: + name: test-selection + path: dist/testselection/ + + - name: Run EventBridge v1 Provider Tests + timeout-minutes: 30 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBUG: 1 + COVERAGE_FILE: ".coverage.events_v1" + TEST_PATH: "tests/aws/services/events/" + JUNIT_REPORTS_FILE: "pytest-junit-events-v1.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} --reruns 3 -o junit_suite_name=events_v1" + PROVIDER_OVERRIDE_EVENTS: "v1" + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-events-v1 + path: | + pytest-junit-events-v1.xml + .coverage.events_v1 + retention-days: 30 + + test-cfn-v2-engine: + name: Test CloudFront Engine v2 + if: ${{ !inputs.onlyAcceptanceTests }} + runs-on: ubuntu-latest + needs: + - test-preflight + - build + timeout-minutes: 60 + env: + COVERAGE_FILE: ".coverage.cloudformation_v2" + JUNIT_REPORTS_FILE: "pytest-junit-cloudformation-v2.xml" + # Set job-specific environment variables for pytest-tinybird + CI_JOB_NAME: ${{ github.job }} + CI_JOB_ID: ${{ github.job }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare Local Test Environment + uses: localstack/localstack/.github/actions/setup-test-env@master + + - name: Download Test Selection + if: ${{ env.TESTSELECTION_PYTEST_ARGS }} + uses: actions/download-artifact@v4 + with: + name: test-selection + path: dist/testselection/ + + - name: Run CloudFormation Engine v2 Tests + timeout-minutes: 30 + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEST_PATH: "tests/aws/services/cloudformation/v2" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }} --reruns 3 -o junit_suite_name='cloudformation_v2'" + PROVIDER_OVERRIDE_CLOUDFORMATION: "engine-v2" + run: make test-coverage + + - name: Archive Test Results + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results-cloudformation-v2 + include-hidden-files: true + path: | + ${{ env.COVERAGE_FILE }} + ${{ env.JUNIT_REPORTS_FILE }} + retention-days: 30 + + capture-not-implemented: + name: "Capture Not Implemented" + if: ${{ !inputs.onlyAcceptanceTests && github.ref == 'refs/heads/master' }} + runs-on: ubuntu-latest + needs: build + env: + PLATFORM: 'amd64' + steps: + - name: Login to Docker Hub + # login to DockerHub to avoid rate limiting issues on custom runners + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + password: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Load Localstack Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: "${{ env.PLATFORM }}" + + - name: Install Community Dependencies + run: make install-dev + + - name: Start LocalStack + env: + # add the GitHub API token to avoid rate limit issues + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISABLE_EVENTS: "1" + DEBUG: 1 + IMAGE_NAME: "localstack/localstack:latest" + run: | + source .venv/bin/activate + localstack start -d + localstack wait -t 120 || (localstack logs && false) + + - name: Run capture-not-implemented + run: | + source .venv/bin/activate + cd scripts + mkdir ../results + python -m capture_notimplemented_responses ../results/ + + - name: Print the logs + run: | + source .venv/bin/activate + localstack logs + + - name: Stop localstack + run: | + source .venv/bin/activate + localstack stop + + - name: Archive Capture-Not-Implemented Results + uses: actions/upload-artifact@v4 + with: + name: capture-notimplemented + path: results/ + retention-days: 30 diff --git a/Makefile b/Makefile index 36f8d4d7598da..b2a749b6599c9 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ start: ## Manually start the local infrastructure for testing ($(VENV_RUN); exec bin/localstack start --host) docker-run-tests: ## Initializes the test environment and runs the tests in a docker container - docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/.git:/opt/code/localstack/.git -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ + docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/.git:/opt/code/localstack/.git -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/dist/:/opt/code/localstack/dist/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME):$(DEFAULT_TAG) \ bash -c "make install-test && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' LAMBDA_IGNORE_ARCHITECTURE=1 LAMBDA_INIT_POST_INVOKE_WAIT_MS=50 TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' TEST_AWS_REGION_NAME='${TEST_AWS_REGION_NAME}' TEST_AWS_ACCESS_KEY_ID='${TEST_AWS_ACCESS_KEY_ID}' TEST_AWS_ACCOUNT_ID='${TEST_AWS_ACCOUNT_ID}' make test-coverage" @@ -110,7 +110,7 @@ docker-cp-coverage: docker rm -v $$id test: ## Run automated tests - ($(VENV_RUN); $(TEST_EXEC) pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) $(PYTEST_ARGS) $(TEST_PATH)) + ($(VENV_RUN); $(TEST_EXEC) pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) --junitxml=$(JUNIT_REPORTS_FILE) $(PYTEST_ARGS) $(TEST_PATH)) test-coverage: LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC = 1 test-coverage: TEST_EXEC = python -m coverage run $(COVERAGE_ARGS) -m From e59a93ee4132cbf52604f48b68ca63f7820f6c98 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 5 May 2025 08:35:58 +0200 Subject: [PATCH 094/108] Update ASF APIs (#12578) Co-authored-by: LocalStack Bot --- .../localstack/aws/api/acm/__init__.py | 17 ++++ .../localstack/aws/api/ec2/__init__.py | 12 ++- .../localstack/aws/api/kinesis/__init__.py | 54 +++++++++-- .../localstack/aws/api/logs/__init__.py | 1 + .../localstack/aws/api/ssm/__init__.py | 94 +++++++++++++++++++ pyproject.toml | 4 +- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +- requirements-runtime.txt | 6 +- requirements-test.txt | 6 +- requirements-typehint.txt | 6 +- 11 files changed, 187 insertions(+), 23 deletions(-) diff --git a/localstack-core/localstack/aws/api/acm/__init__.py b/localstack-core/localstack/aws/api/acm/__init__.py index f3e00c58471e6..dc9585748f9f7 100644 --- a/localstack-core/localstack/aws/api/acm/__init__.py +++ b/localstack-core/localstack/aws/api/acm/__init__.py @@ -23,6 +23,10 @@ ValidationExceptionMessage = str +class CertificateManagedBy(StrEnum): + CLOUDFRONT = "CLOUDFRONT" + + class CertificateStatus(StrEnum): PENDING_VALIDATION = "PENDING_VALIDATION" ISSUED = "ISSUED" @@ -131,6 +135,7 @@ class RevocationReason(StrEnum): CA_COMPROMISE = "CA_COMPROMISE" AFFILIATION_CHANGED = "AFFILIATION_CHANGED" SUPERCEDED = "SUPERCEDED" + SUPERSEDED = "SUPERSEDED" CESSATION_OF_OPERATION = "CESSATION_OF_OPERATION" CERTIFICATE_HOLD = "CERTIFICATE_HOLD" REMOVE_FROM_CRL = "REMOVE_FROM_CRL" @@ -150,6 +155,7 @@ class SortOrder(StrEnum): class ValidationMethod(StrEnum): EMAIL = "EMAIL" DNS = "DNS" + HTTP = "HTTP" class AccessDeniedException(ServiceException): @@ -285,6 +291,11 @@ class KeyUsage(TypedDict, total=False): TStamp = datetime +class HttpRedirect(TypedDict, total=False): + RedirectFrom: Optional[String] + RedirectTo: Optional[String] + + class ResourceRecord(TypedDict, total=False): Name: String Type: RecordType @@ -300,6 +311,7 @@ class DomainValidation(TypedDict, total=False): ValidationDomain: Optional[DomainNameString] ValidationStatus: Optional[DomainStatus] ResourceRecord: Optional[ResourceRecord] + HttpRedirect: Optional[HttpRedirect] ValidationMethod: Optional[ValidationMethod] @@ -321,6 +333,7 @@ class CertificateDetail(TypedDict, total=False): CertificateArn: Optional[Arn] DomainName: Optional[DomainNameString] SubjectAlternativeNames: Optional[DomainList] + ManagedBy: Optional[CertificateManagedBy] DomainValidationOptions: Optional[DomainValidationList] Serial: Optional[String] Subject: Optional[String] @@ -370,6 +383,7 @@ class CertificateSummary(TypedDict, total=False): IssuedAt: Optional[TStamp] ImportedAt: Optional[TStamp] RevokedAt: Optional[TStamp] + ManagedBy: Optional[CertificateManagedBy] CertificateSummaryList = List[CertificateSummary] @@ -422,6 +436,7 @@ class Filters(TypedDict, total=False): extendedKeyUsage: Optional[ExtendedKeyUsageFilterList] keyUsage: Optional[KeyUsageFilterList] keyTypes: Optional[KeyAlgorithmList] + managedBy: Optional[CertificateManagedBy] class GetAccountConfigurationResponse(TypedDict, total=False): @@ -498,6 +513,7 @@ class RequestCertificateRequest(ServiceRequest): CertificateAuthorityArn: Optional[PcaArn] Tags: Optional[TagList] KeyAlgorithm: Optional[KeyAlgorithm] + ManagedBy: Optional[CertificateManagedBy] class RequestCertificateResponse(TypedDict, total=False): @@ -619,6 +635,7 @@ def request_certificate( certificate_authority_arn: PcaArn = None, tags: TagList = None, key_algorithm: KeyAlgorithm = None, + managed_by: CertificateManagedBy = None, **kwargs, ) -> RequestCertificateResponse: raise NotImplementedError diff --git a/localstack-core/localstack/aws/api/ec2/__init__.py b/localstack-core/localstack/aws/api/ec2/__init__.py index 51bba5a545c69..2f92ebd70813b 100644 --- a/localstack-core/localstack/aws/api/ec2/__init__.py +++ b/localstack-core/localstack/aws/api/ec2/__init__.py @@ -2316,6 +2316,11 @@ class IpamManagementState(StrEnum): ignored = "ignored" +class IpamMeteredAccount(StrEnum): + ipam_owner = "ipam-owner" + resource_owner = "resource-owner" + + class IpamNetworkInterfaceAttachmentStatus(StrEnum): available = "available" in_use = "in-use" @@ -7012,7 +7017,7 @@ class FleetLaunchTemplateOverridesRequest(TypedDict, total=False): Placement: Optional[Placement] BlockDeviceMappings: Optional[FleetBlockDeviceMappingRequestList] InstanceRequirements: Optional[InstanceRequirementsRequest] - ImageId: Optional[String] + ImageId: Optional[ImageId] FleetLaunchTemplateOverridesListRequest = List[FleetLaunchTemplateOverridesRequest] @@ -7382,6 +7387,7 @@ class CreateIpamRequest(ServiceRequest): ClientToken: Optional[String] Tier: Optional[IpamTier] EnablePrivateGua: Optional[Boolean] + MeteredAccount: Optional[IpamMeteredAccount] class CreateIpamResourceDiscoveryRequest(ServiceRequest): @@ -7441,6 +7447,7 @@ class Ipam(TypedDict, total=False): StateMessage: Optional[String] Tier: Optional[IpamTier] EnablePrivateGua: Optional[Boolean] + MeteredAccount: Optional[IpamMeteredAccount] class CreateIpamResult(TypedDict, total=False): @@ -18368,6 +18375,7 @@ class ModifyIpamRequest(ServiceRequest): RemoveOperatingRegions: Optional[RemoveIpamOperatingRegionSet] Tier: Optional[IpamTier] EnablePrivateGua: Optional[Boolean] + MeteredAccount: Optional[IpamMeteredAccount] class ModifyIpamResourceCidrRequest(ServiceRequest): @@ -21212,6 +21220,7 @@ def create_ipam( client_token: String = None, tier: IpamTier = None, enable_private_gua: Boolean = None, + metered_account: IpamMeteredAccount = None, **kwargs, ) -> CreateIpamResult: raise NotImplementedError @@ -26845,6 +26854,7 @@ def modify_ipam( remove_operating_regions: RemoveIpamOperatingRegionSet = None, tier: IpamTier = None, enable_private_gua: Boolean = None, + metered_account: IpamMeteredAccount = None, **kwargs, ) -> ModifyIpamResult: raise NotImplementedError diff --git a/localstack-core/localstack/aws/api/kinesis/__init__.py b/localstack-core/localstack/aws/api/kinesis/__init__.py index 515ac108c7dba..738a00f12cad1 100644 --- a/localstack-core/localstack/aws/api/kinesis/__init__.py +++ b/localstack-core/localstack/aws/api/kinesis/__init__.py @@ -494,11 +494,8 @@ class ListStreamsOutput(TypedDict, total=False): StreamSummaries: Optional[StreamSummaryList] -class ListTagsForStreamInput(ServiceRequest): - StreamName: Optional[StreamName] - ExclusiveStartTagKey: Optional[TagKey] - Limit: Optional[ListTagsForStreamInputLimit] - StreamARN: Optional[StreamARN] +class ListTagsForResourceInput(ServiceRequest): + ResourceARN: ResourceARN class Tag(TypedDict, total=False): @@ -509,6 +506,17 @@ class Tag(TypedDict, total=False): TagList = List[Tag] +class ListTagsForResourceOutput(TypedDict, total=False): + Tags: Optional[TagList] + + +class ListTagsForStreamInput(ServiceRequest): + StreamName: Optional[StreamName] + ExclusiveStartTagKey: Optional[TagKey] + Limit: Optional[ListTagsForStreamInputLimit] + StreamARN: Optional[StreamARN] + + class ListTagsForStreamOutput(TypedDict, total=False): Tags: TagList HasMoreTags: BooleanObject @@ -575,6 +583,7 @@ class PutResourcePolicyInput(ServiceRequest): class RegisterStreamConsumerInput(ServiceRequest): StreamARN: StreamARN ConsumerName: ConsumerName + Tags: Optional[TagMap] class RegisterStreamConsumerOutput(TypedDict, total=False): @@ -647,6 +656,16 @@ class SubscribeToShardOutput(TypedDict, total=False): EventStream: Iterator[SubscribeToShardEventStream] +class TagResourceInput(ServiceRequest): + Tags: TagMap + ResourceARN: ResourceARN + + +class UntagResourceInput(ServiceRequest): + TagKeys: TagKeyList + ResourceARN: ResourceARN + + class UpdateShardCountInput(ServiceRequest): StreamName: Optional[StreamName] TargetShardCount: PositiveIntegerObject @@ -871,6 +890,12 @@ def list_streams( ) -> ListStreamsOutput: raise NotImplementedError + @handler("ListTagsForResource") + def list_tags_for_resource( + self, context: RequestContext, resource_arn: ResourceARN, **kwargs + ) -> ListTagsForResourceOutput: + raise NotImplementedError + @handler("ListTagsForStream") def list_tags_for_stream( self, @@ -928,7 +953,12 @@ def put_resource_policy( @handler("RegisterStreamConsumer") def register_stream_consumer( - self, context: RequestContext, stream_arn: StreamARN, consumer_name: ConsumerName, **kwargs + self, + context: RequestContext, + stream_arn: StreamARN, + consumer_name: ConsumerName, + tags: TagMap = None, + **kwargs, ) -> RegisterStreamConsumerOutput: raise NotImplementedError @@ -990,6 +1020,18 @@ def subscribe_to_shard( ) -> SubscribeToShardOutput: raise NotImplementedError + @handler("TagResource") + def tag_resource( + self, context: RequestContext, tags: TagMap, resource_arn: ResourceARN, **kwargs + ) -> None: + raise NotImplementedError + + @handler("UntagResource") + def untag_resource( + self, context: RequestContext, tag_keys: TagKeyList, resource_arn: ResourceARN, **kwargs + ) -> None: + raise NotImplementedError + @handler("UpdateShardCount") def update_shard_count( self, diff --git a/localstack-core/localstack/aws/api/logs/__init__.py b/localstack-core/localstack/aws/api/logs/__init__.py index 3a22676f4bbeb..2d39131abbb85 100644 --- a/localstack-core/localstack/aws/api/logs/__init__.py +++ b/localstack-core/localstack/aws/api/logs/__init__.py @@ -231,6 +231,7 @@ class IntegrationType(StrEnum): class LogGroupClass(StrEnum): STANDARD = "STANDARD" INFREQUENT_ACCESS = "INFREQUENT_ACCESS" + DELIVERY = "DELIVERY" class OpenSearchResourceStatusType(StrEnum): diff --git a/localstack-core/localstack/aws/api/ssm/__init__.py b/localstack-core/localstack/aws/api/ssm/__init__.py index ec192fa21d5bf..a2e95b19d9538 100644 --- a/localstack-core/localstack/aws/api/ssm/__init__.py +++ b/localstack-core/localstack/aws/api/ssm/__init__.py @@ -4,6 +4,9 @@ from localstack.aws.api import RequestContext, ServiceException, ServiceRequest, handler +AccessKeyIdType = str +AccessKeySecretType = str +AccessRequestId = str Account = str AccountId = str ActivationCode = str @@ -349,6 +352,7 @@ SessionOwner = str SessionReason = str SessionTarget = str +SessionTokenType = str SharedDocumentVersion = str SnapshotDownloadUrl = str SnapshotId = str @@ -362,6 +366,7 @@ StepExecutionFilterValue = str StreamUrl = str String = str +String1to256 = str StringDateTime = str TagKey = str TagValue = str @@ -381,6 +386,14 @@ Version = str +class AccessRequestStatus(StrEnum): + Approved = "Approved" + Rejected = "Rejected" + Revoked = "Revoked" + Expired = "Expired" + Pending = "Pending" + + class AssociationComplianceSeverity(StrEnum): CRITICAL = "CRITICAL" HIGH = "HIGH" @@ -478,6 +491,7 @@ class AutomationExecutionStatus(StrEnum): class AutomationSubtype(StrEnum): ChangeRequest = "ChangeRequest" + AccessRequest = "AccessRequest" class AutomationType(StrEnum): @@ -632,6 +646,8 @@ class DocumentType(StrEnum): CloudFormation = "CloudFormation" ConformancePackTemplate = "ConformancePackTemplate" QuickSetup = "QuickSetup" + ManualApprovalPolicy = "ManualApprovalPolicy" + AutoApprovalPolicy = "AutoApprovalPolicy" class ExecutionMode(StrEnum): @@ -881,6 +897,15 @@ class OpsItemFilterKey(StrEnum): Category = "Category" Severity = "Severity" OpsItemType = "OpsItemType" + AccessRequestByRequesterArn = "AccessRequestByRequesterArn" + AccessRequestByRequesterId = "AccessRequestByRequesterId" + AccessRequestByApproverArn = "AccessRequestByApproverArn" + AccessRequestByApproverId = "AccessRequestByApproverId" + AccessRequestBySourceAccountId = "AccessRequestBySourceAccountId" + AccessRequestBySourceOpsItemId = "AccessRequestBySourceOpsItemId" + AccessRequestBySourceRegion = "AccessRequestBySourceRegion" + AccessRequestByIsReplica = "AccessRequestByIsReplica" + AccessRequestByTargetResourceId = "AccessRequestByTargetResourceId" ChangeRequestByRequesterArn = "ChangeRequestByRequesterArn" ChangeRequestByRequesterName = "ChangeRequestByRequesterName" ChangeRequestByApproverArn = "ChangeRequestByApproverArn" @@ -926,6 +951,7 @@ class OpsItemStatus(StrEnum): ChangeCalendarOverrideRejected = "ChangeCalendarOverrideRejected" PendingApproval = "PendingApproval" Approved = "Approved" + Revoked = "Revoked" Rejected = "Rejected" Closed = "Closed" @@ -1100,6 +1126,7 @@ class SignalType(StrEnum): StartStep = "StartStep" StopStep = "StopStep" Resume = "Resume" + Revoke = "Revoke" class SourceType(StrEnum): @@ -1125,6 +1152,12 @@ class StopType(StrEnum): Cancel = "Cancel" +class AccessDeniedException(ServiceException): + code: str = "AccessDeniedException" + sender_fault: bool = False + status_code: int = 400 + + class AlreadyExistsException(ServiceException): code: str = "AlreadyExistsException" sender_fault: bool = False @@ -1855,6 +1888,16 @@ class ResourcePolicyNotFoundException(ServiceException): status_code: int = 400 +class ServiceQuotaExceededException(ServiceException): + code: str = "ServiceQuotaExceededException" + sender_fault: bool = False + status_code: int = 400 + ResourceId: Optional[String] + ResourceType: Optional[String] + QuotaCode: String + ServiceCode: String + + class ServiceSettingNotFound(ServiceException): code: str = "ServiceSettingNotFound" sender_fault: bool = False @@ -1885,6 +1928,14 @@ class TargetNotConnected(ServiceException): status_code: int = 400 +class ThrottlingException(ServiceException): + code: str = "ThrottlingException" + sender_fault: bool = False + status_code: int = 400 + QuotaCode: Optional[String] + ServiceCode: Optional[String] + + class TooManyTagsError(ServiceException): code: str = "TooManyTagsError" sender_fault: bool = False @@ -3034,6 +3085,13 @@ class CreateResourceDataSyncResult(TypedDict, total=False): pass +class Credentials(TypedDict, total=False): + AccessKeyId: AccessKeyIdType + SecretAccessKey: AccessKeySecretType + SessionToken: SessionTokenType + ExpirationTime: DateTime + + class DeleteActivationRequest(ServiceRequest): ActivationId: ActivationId @@ -4285,6 +4343,15 @@ class ExecutionPreview(TypedDict, total=False): Automation: Optional[AutomationExecutionPreview] +class GetAccessTokenRequest(ServiceRequest): + AccessRequestId: AccessRequestId + + +class GetAccessTokenResponse(TypedDict, total=False): + Credentials: Optional[Credentials] + AccessRequestStatus: Optional[AccessRequestStatus] + + class GetAutomationExecutionRequest(ServiceRequest): AutomationExecutionId: AutomationExecutionId @@ -5536,6 +5603,16 @@ class SendCommandResult(TypedDict, total=False): SessionManagerParameters = Dict[SessionManagerParameterName, SessionManagerParameterValueList] +class StartAccessRequestRequest(ServiceRequest): + Reason: String1to256 + Targets: Targets + Tags: Optional[TagList] + + +class StartAccessRequestResponse(TypedDict, total=False): + AccessRequestId: Optional[AccessRequestId] + + class StartAssociationsOnceRequest(ServiceRequest): AssociationIds: AssociationIdList @@ -6613,6 +6690,12 @@ def disassociate_ops_item_related_item( ) -> DisassociateOpsItemRelatedItemResponse: raise NotImplementedError + @handler("GetAccessToken") + def get_access_token( + self, context: RequestContext, access_request_id: AccessRequestId, **kwargs + ) -> GetAccessTokenResponse: + raise NotImplementedError + @handler("GetAutomationExecution") def get_automation_execution( self, context: RequestContext, automation_execution_id: AutomationExecutionId, **kwargs @@ -7251,6 +7334,17 @@ def send_command( ) -> SendCommandResult: raise NotImplementedError + @handler("StartAccessRequest") + def start_access_request( + self, + context: RequestContext, + reason: String1to256, + targets: Targets, + tags: TagList = None, + **kwargs, + ) -> StartAccessRequestResponse: + raise NotImplementedError + @handler("StartAssociationsOnce") def start_associations_once( self, context: RequestContext, association_ids: AssociationIdList, **kwargs diff --git a/pyproject.toml b/pyproject.toml index f6f8acab0c02a..167f2eab315a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.38.0", + "boto3==1.38.8", # pinned / updated by ASF update action - "botocore==1.38.0", + "botocore==1.38.8", "awscrt>=0.13.14", "cbor2>=5.5.0", "dnspython>=1.16.0", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index b43a075b4432f..eb5de1211048a 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -11,9 +11,9 @@ attrs==25.3.0 # referencing awscrt==0.27.0 # via localstack-core (pyproject.toml) -boto3==1.38.0 +boto3==1.38.8 # via localstack-core (pyproject.toml) -botocore==1.38.0 +botocore==1.38.8 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5996d434a48c8..e7c4a8a25e1d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.39.0 +awscli==1.40.7 # via localstack-core awscrt==0.27.0 # via localstack-core -boto3==1.38.0 +boto3==1.38.8 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.38.0 +botocore==1.38.8 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index ae8018c286d54..45ef1c466c6d8 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -29,17 +29,17 @@ aws-sam-translator==1.97.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.39.0 +awscli==1.40.7 # via localstack-core (pyproject.toml) awscrt==0.27.0 # via localstack-core -boto3==1.38.0 +boto3==1.38.8 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.38.0 +botocore==1.38.8 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 9fa7f02cddd32..28b4c30413092 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -41,17 +41,17 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.39.0 +awscli==1.40.7 # via localstack-core awscrt==0.27.0 # via localstack-core -boto3==1.38.0 +boto3==1.38.8 # via # amazon-kclpy # aws-sam-translator # localstack-core # moto-ext -botocore==1.38.0 +botocore==1.38.8 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index a9583aa8836c4..2d9998835c14a 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -41,11 +41,11 @@ aws-sam-translator==1.97.0 # localstack-core aws-xray-sdk==2.14.0 # via moto-ext -awscli==1.39.0 +awscli==1.40.7 # via localstack-core awscrt==0.27.0 # via localstack-core -boto3==1.38.0 +boto3==1.38.8 # via # amazon-kclpy # aws-sam-translator @@ -53,7 +53,7 @@ boto3==1.38.0 # moto-ext boto3-stubs==1.38.4 # via localstack-core (pyproject.toml) -botocore==1.38.0 +botocore==1.38.8 # via # aws-xray-sdk # awscli From 5c66c51dce0b9411e95f507f89d1c0c4ef126c59 Mon Sep 17 00:00:00 2001 From: Vittorio Polverino Date: Mon, 5 May 2025 09:43:45 +0200 Subject: [PATCH 095/108] remove usage analytics module (#12573) --- .../localstack/utils/analytics/usage.py | 169 ------------------ localstack-core/localstack/utils/diagnose.py | 4 +- tests/unit/utils/analytics/test_usage.py | 9 - 3 files changed, 2 insertions(+), 180 deletions(-) delete mode 100644 localstack-core/localstack/utils/analytics/usage.py delete mode 100644 tests/unit/utils/analytics/test_usage.py diff --git a/localstack-core/localstack/utils/analytics/usage.py b/localstack-core/localstack/utils/analytics/usage.py deleted file mode 100644 index 3c7abd64c7024..0000000000000 --- a/localstack-core/localstack/utils/analytics/usage.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -[DEPRECATED] This module is deprecated in favor of `localstack.utils.analytics.metrics`. -""" - -import datetime -import math -from collections import defaultdict -from itertools import count -from typing import Any - -from localstack import config -from localstack.runtime import hooks -from localstack.utils.analytics import get_session_id -from localstack.utils.analytics.events import Event, EventMetadata -from localstack.utils.analytics.publisher import AnalyticsClientPublisher - -# Counters have to register with the registry -collector_registry: dict[str, Any] = dict() - - -# TODO: introduce some base abstraction for the counters after gather some initial experience working with it -# we could probably do intermediate aggregations over time to avoid unbounded counters for very long LS sessions -# for now, we can recommend to use config.DISABLE_EVENTS=1 - - -class UsageSetCounter: - """ - [DEPRECATED] Use `localstack.utils.analytics.metrics.Counter` instead. - Use this counter to count occurrences of unique values - - Example: - my_feature_counter = UsageSetCounter("lambda:runtime") - my_feature_counter.record("python3.7") - my_feature_counter.record("nodejs16.x") - my_feature_counter.record("nodejs16.x") - my_feature_counter.aggregate() # returns {"python3.7": 1, "nodejs16.x": 2} - """ - - state: dict[str, int] - _counter: dict[str, count] - namespace: str - - def __init__(self, namespace: str): - self.enabled = not config.DISABLE_EVENTS - self.state = {} - self._counter = defaultdict(lambda: count(1)) - self.namespace = namespace - collector_registry[namespace] = self - - def record(self, value: str): - if self.enabled: - self.state[value] = next(self._counter[value]) - - def aggregate(self) -> dict: - return self.state - - -class UsageCounter: - """ - [DEPRECATED] Use `localstack.utils.analytics.metrics.Counter` instead. - Use this counter to count numeric values - - Example: - my__counter = UsageCounter("lambda:somefeature") - my_counter.increment() - my_counter.increment() - my_counter.aggregate() # returns {"count": 2} - """ - - state: int - namespace: str - - def __init__(self, namespace: str): - self.enabled = not config.DISABLE_EVENTS - self.state = 0 - self._counter = count(1) - self.namespace = namespace - collector_registry[namespace] = self - - def increment(self): - # TODO: we should instead have different underlying datastructures to store the state, and have no-op operations - # when config.DISABLE_EVENTS is set - if self.enabled: - self.state = next(self._counter) - - def aggregate(self) -> dict: - # TODO: should we just keep `count`? "sum" might need to be kept for historical data? - return {"count": self.state, "sum": self.state} - - -class TimingStats: - """ - Use this counter to measure numeric values and perform aggregations - - Available aggregations: min, max, sum, mean, median, count - - Example: - my_feature_counter = TimingStats("lambda:somefeature", aggregations=["min", "max", "sum", "count"]) - my_feature_counter.record_value(512) - my_feature_counter.record_value(256) - my_feature_counter.aggregate() # returns {"min": 256, "max": 512, "sum": 768, "count": 2} - """ - - state: list[int | float] - namespace: str - aggregations: list[str] - - def __init__(self, namespace: str, aggregations: list[str]): - self.enabled = not config.DISABLE_EVENTS - self.state = [] - self.namespace = namespace - self.aggregations = aggregations - collector_registry[namespace] = self - - def record_value(self, value: int | float): - if self.enabled: - self.state.append(value) - - def aggregate(self) -> dict: - result = {} - if self.state: - for aggregation in self.aggregations: - if aggregation == "sum": - result[aggregation] = sum(self.state) - elif aggregation == "min": - result[aggregation] = min(self.state) - elif aggregation == "max": - result[aggregation] = max(self.state) - elif aggregation == "mean": - result[aggregation] = sum(self.state) / len(self.state) - elif aggregation == "median": - median_index = math.floor(len(self.state) / 2) - result[aggregation] = sorted(self.state)[median_index] - elif aggregation == "count": - result[aggregation] = len(self.state) - else: - raise Exception(f"Unsupported aggregation: {aggregation}") - return result - - -def aggregate() -> dict: - aggregated_payload = {} - for ns, collector in collector_registry.items(): - agg = collector.aggregate() - if agg: - aggregated_payload[ns] = agg - return aggregated_payload - - -@hooks.on_infra_shutdown() -def aggregate_and_send(): - """ - Aggregates data from all registered usage trackers and immediately sends the aggregated result to the analytics service. - """ - if config.DISABLE_EVENTS: - return - - metadata = EventMetadata( - session_id=get_session_id(), - client_time=str(datetime.datetime.now()), - ) - - aggregated_payload = aggregate() - - if aggregated_payload: - publisher = AnalyticsClientPublisher() - publisher.publish( - [Event(name="ls:usage_analytics", metadata=metadata, payload=aggregated_payload)] - ) diff --git a/localstack-core/localstack/utils/diagnose.py b/localstack-core/localstack/utils/diagnose.py index 0de08f10d5ca0..36b0b079631f9 100644 --- a/localstack-core/localstack/utils/diagnose.py +++ b/localstack-core/localstack/utils/diagnose.py @@ -10,7 +10,7 @@ from localstack.services.lambda_.invocation.docker_runtime_executor import IMAGE_PREFIX from localstack.services.lambda_.runtimes import IMAGE_MAPPING from localstack.utils import bootstrap -from localstack.utils.analytics import usage +from localstack.utils.analytics.metrics import MetricRegistry from localstack.utils.container_networking import get_main_container_name from localstack.utils.container_utils.container_client import ContainerException, NoSuchImage from localstack.utils.docker_utils import DOCKER_CLIENT @@ -153,4 +153,4 @@ def get_host_kernel_version() -> str: def get_usage(): - return usage.aggregate() + return MetricRegistry().collect() diff --git a/tests/unit/utils/analytics/test_usage.py b/tests/unit/utils/analytics/test_usage.py deleted file mode 100644 index 5d7cfd9fe8058..0000000000000 --- a/tests/unit/utils/analytics/test_usage.py +++ /dev/null @@ -1,9 +0,0 @@ -from localstack.utils.analytics.usage import UsageSetCounter - - -def test_set_counter(): - my_feature_counter = UsageSetCounter("lambda:runtime") - my_feature_counter.record("python3.7") - my_feature_counter.record("nodejs16.x") - my_feature_counter.record("nodejs16.x") - assert my_feature_counter.aggregate() == {"python3.7": 1, "nodejs16.x": 2} From 9b55514cc680b340a04affc6910968e554ca7589 Mon Sep 17 00:00:00 2001 From: Silvio Vasiljevic Date: Mon, 5 May 2025 14:45:02 +0200 Subject: [PATCH 096/108] Switch to using `kclpy-ext` (#12567) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 3 ++- requirements-base-runtime.txt | 4 ++-- requirements-basic.txt | 2 +- requirements-dev.txt | 24 +++++++++---------- requirements-runtime.txt | 18 +++++++------- requirements-test.txt | 22 +++++++++--------- requirements-typehint.txt | 44 +++++++++++++++++------------------ 8 files changed, 60 insertions(+), 59 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce8be6924df84..0e14866fcce53 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/pyproject.toml b/pyproject.toml index 167f2eab315a0..fb920801351d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,8 @@ runtime = [ # pinned / updated by ASF update action "awscli>=1.37.0", "airspeed-ext>=0.6.3", - "amazon_kclpy>=3.0.0", + # version that has a built wheel + "kclpy-ext>=3.0.0", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code "antlr4-python3-runtime==4.13.2", "apispec>=5.1.1", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index eb5de1211048a..d8fcc647bd99c 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -9,7 +9,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -awscrt==0.27.0 +awscrt==0.26.1 # via localstack-core (pyproject.toml) boto3==1.38.8 # via localstack-core (pyproject.toml) @@ -28,7 +28,7 @@ certifi==2025.4.26 # via requests cffi==1.17.1 # via cryptography -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via localstack-core (pyproject.toml) diff --git a/requirements-basic.txt b/requirements-basic.txt index e70ca59967c46..20ea505e5ffce 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -12,7 +12,7 @@ certifi==2025.4.26 # via requests cffi==1.17.1 # via cryptography -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index e7c4a8a25e1d3..026f21d135944 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,8 +6,6 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.3 - # via localstack-core annotated-types==0.7.0 # via pydantic antlr4-python3-runtime==4.13.2 @@ -19,7 +17,7 @@ anyio==4.9.0 apispec==6.8.1 # via localstack-core argparse==1.4.0 - # via amazon-kclpy + # via kclpy-ext attrs==25.3.0 # via # cattrs @@ -27,13 +25,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.233 +aws-cdk-asset-awscli-v1==2.2.234 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.192.0 +aws-cdk-lib==2.194.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -43,12 +41,12 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.40.7 # via localstack-core -awscrt==0.27.0 +awscrt==0.26.1 # via localstack-core boto3==1.38.8 # via - # amazon-kclpy # aws-sam-translator + # kclpy-ext # localstack-core # moto-ext botocore==1.38.8 @@ -84,7 +82,7 @@ cfgv==3.4.0 # via pre-commit cfn-lint==1.34.2 # via moto-ext -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via @@ -234,6 +232,8 @@ jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator +kclpy-ext==3.0.3 + # via localstack-core lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 @@ -335,13 +335,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.3 +pydantic==2.11.4 # via aws-sam-translator -pydantic-core==2.33.1 +pydantic-core==2.33.2 # via pydantic pygments==2.19.1 # via rich -pymongo==4.12.0 +pymongo==4.12.1 # via localstack-core pyopenssl==25.0.0 # via @@ -429,7 +429,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.11.7 +ruff==0.11.8 # via localstack-core (pyproject.toml) s3transfer==0.12.0 # via diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 45ef1c466c6d8..9b44aafa471c3 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -6,8 +6,6 @@ # airspeed-ext==0.6.7 # via localstack-core (pyproject.toml) -amazon-kclpy==3.0.3 - # via localstack-core (pyproject.toml) annotated-types==0.7.0 # via pydantic antlr4-python3-runtime==4.13.2 @@ -17,7 +15,7 @@ antlr4-python3-runtime==4.13.2 apispec==6.8.1 # via localstack-core (pyproject.toml) argparse==1.4.0 - # via amazon-kclpy + # via kclpy-ext attrs==25.3.0 # via # jsonschema @@ -31,12 +29,12 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.40.7 # via localstack-core (pyproject.toml) -awscrt==0.27.0 +awscrt==0.26.1 # via localstack-core boto3==1.38.8 # via - # amazon-kclpy # aws-sam-translator + # kclpy-ext # localstack-core # moto-ext botocore==1.38.8 @@ -66,7 +64,7 @@ cffi==1.17.1 # via cryptography cfn-lint==1.34.2 # via moto-ext -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via @@ -174,6 +172,8 @@ jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator +kclpy-ext==3.0.3 + # via localstack-core (pyproject.toml) lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-twisted==24.3.0 @@ -239,13 +239,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.3 +pydantic==2.11.4 # via aws-sam-translator -pydantic-core==2.33.1 +pydantic-core==2.33.2 # via pydantic pygments==2.19.1 # via rich -pymongo==4.12.0 +pymongo==4.12.1 # via localstack-core (pyproject.toml) pyopenssl==25.0.0 # via diff --git a/requirements-test.txt b/requirements-test.txt index 28b4c30413092..816cafabd5a10 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,8 +6,6 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.3 - # via localstack-core annotated-types==0.7.0 # via pydantic antlr4-python3-runtime==4.13.2 @@ -19,7 +17,7 @@ anyio==4.9.0 apispec==6.8.1 # via localstack-core argparse==1.4.0 - # via amazon-kclpy + # via kclpy-ext attrs==25.3.0 # via # cattrs @@ -27,13 +25,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.233 +aws-cdk-asset-awscli-v1==2.2.234 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.192.0 +aws-cdk-lib==2.194.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.97.0 # via @@ -43,12 +41,12 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.40.7 # via localstack-core -awscrt==0.27.0 +awscrt==0.26.1 # via localstack-core boto3==1.38.8 # via - # amazon-kclpy # aws-sam-translator + # kclpy-ext # localstack-core # moto-ext botocore==1.38.8 @@ -82,7 +80,7 @@ cffi==1.17.1 # via cryptography cfn-lint==1.34.2 # via moto-ext -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via @@ -218,6 +216,8 @@ jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator +kclpy-ext==3.0.3 + # via localstack-core lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 @@ -301,13 +301,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.3 +pydantic==2.11.4 # via aws-sam-translator -pydantic-core==2.33.1 +pydantic-core==2.33.2 # via pydantic pygments==2.19.1 # via rich -pymongo==4.12.0 +pymongo==4.12.1 # via localstack-core pyopenssl==25.0.0 # via diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 2d9998835c14a..378c1be5a17c8 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -6,8 +6,6 @@ # airspeed-ext==0.6.7 # via localstack-core -amazon-kclpy==3.0.3 - # via localstack-core annotated-types==0.7.0 # via pydantic antlr4-python3-runtime==4.13.2 @@ -19,7 +17,7 @@ anyio==4.9.0 apispec==6.8.1 # via localstack-core argparse==1.4.0 - # via amazon-kclpy + # via kclpy-ext attrs==25.3.0 # via # cattrs @@ -27,13 +25,13 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.233 +aws-cdk-asset-awscli-v1==2.2.234 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==41.2.0 # via aws-cdk-lib -aws-cdk-lib==2.192.0 +aws-cdk-lib==2.194.0 # via localstack-core aws-sam-translator==1.97.0 # via @@ -43,15 +41,15 @@ aws-xray-sdk==2.14.0 # via moto-ext awscli==1.40.7 # via localstack-core -awscrt==0.27.0 +awscrt==0.26.1 # via localstack-core boto3==1.38.8 # via - # amazon-kclpy # aws-sam-translator + # kclpy-ext # localstack-core # moto-ext -boto3-stubs==1.38.4 +boto3-stubs==1.38.7 # via localstack-core (pyproject.toml) botocore==1.38.8 # via @@ -61,7 +59,7 @@ botocore==1.38.8 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.38.4 +botocore-stubs==1.38.7 # via boto3-stubs build==1.2.2.post1 # via @@ -88,7 +86,7 @@ cfgv==3.4.0 # via pre-commit cfn-lint==1.34.2 # via moto-ext -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests click==8.1.8 # via @@ -238,6 +236,8 @@ jsonschema-specifications==2025.4.1 # via # jsonschema # openapi-schema-validator +kclpy-ext==3.0.3 + # via localstack-core lazy-object-proxy==1.11.0 # via openapi-spec-validator localstack-snapshot==0.2.0 @@ -272,7 +272,7 @@ mypy-boto3-apigateway==1.38.0 # via boto3-stubs mypy-boto3-apigatewayv2==1.38.0 # via boto3-stubs -mypy-boto3-appconfig==1.38.0 +mypy-boto3-appconfig==1.38.7 # via boto3-stubs mypy-boto3-appconfigdata==1.38.0 # via boto3-stubs @@ -324,9 +324,9 @@ mypy-boto3-dynamodb==1.38.4 # via boto3-stubs mypy-boto3-dynamodbstreams==1.38.0 # via boto3-stubs -mypy-boto3-ec2==1.38.0 +mypy-boto3-ec2==1.38.6 # via boto3-stubs -mypy-boto3-ecr==1.38.0 +mypy-boto3-ecr==1.38.6 # via boto3-stubs mypy-boto3-ecs==1.38.3 # via boto3-stubs @@ -370,7 +370,7 @@ mypy-boto3-iotwireless==1.38.0 # via boto3-stubs mypy-boto3-kafka==1.38.0 # via boto3-stubs -mypy-boto3-kinesis==1.38.0 +mypy-boto3-kinesis==1.38.5 # via boto3-stubs mypy-boto3-kinesisanalytics==1.38.0 # via boto3-stubs @@ -382,7 +382,7 @@ mypy-boto3-lakeformation==1.38.0 # via boto3-stubs mypy-boto3-lambda==1.38.0 # via boto3-stubs -mypy-boto3-logs==1.38.0 +mypy-boto3-logs==1.38.6 # via boto3-stubs mypy-boto3-managedblockchain==1.38.0 # via boto3-stubs @@ -430,7 +430,7 @@ mypy-boto3-s3==1.38.0 # via boto3-stubs mypy-boto3-s3control==1.38.0 # via boto3-stubs -mypy-boto3-sagemaker==1.38.0 +mypy-boto3-sagemaker==1.38.7 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.38.0 # via boto3-stubs @@ -448,7 +448,7 @@ mypy-boto3-sns==1.38.0 # via boto3-stubs mypy-boto3-sqs==1.38.0 # via boto3-stubs -mypy-boto3-ssm==1.38.0 +mypy-boto3-ssm==1.38.5 # via boto3-stubs mypy-boto3-sso-admin==1.38.0 # via boto3-stubs @@ -462,7 +462,7 @@ mypy-boto3-timestream-write==1.38.0 # via boto3-stubs mypy-boto3-transcribe==1.38.0 # via boto3-stubs -mypy-boto3-verifiedpermissions==1.38.0 +mypy-boto3-verifiedpermissions==1.38.7 # via boto3-stubs mypy-boto3-wafv2==1.38.0 # via boto3-stubs @@ -545,13 +545,13 @@ pyasn1==0.6.1 # via rsa pycparser==2.22 # via cffi -pydantic==2.11.3 +pydantic==2.11.4 # via aws-sam-translator -pydantic-core==2.33.1 +pydantic-core==2.33.2 # via pydantic pygments==2.19.1 # via rich -pymongo==4.12.0 +pymongo==4.12.1 # via localstack-core pyopenssl==25.0.0 # via @@ -639,7 +639,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.11.7 +ruff==0.11.8 # via localstack-core s3transfer==0.12.0 # via From d95c5569b606e197e7264721710db11cc6120b6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 08:34:28 +0200 Subject: [PATCH 097/108] Bump python from `82c07f2` to `75a17dd` in the docker-base-images group (#12582) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- Dockerfile.s3 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d75a5b7205db3..7cfac6990a339 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # # base: Stage which installs necessary runtime dependencies (OS packages, etc.) # -FROM python:3.11.12-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS base +FROM python:3.11.12-slim-bookworm@sha256:75a17dd6f00b277975715fc094c4a1570d512708de6bb4c5dc130814813ebfe4 AS base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index 98e82f396e9d3..c53190ee7529d 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.12-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS base +FROM python:3.11.12-slim-bookworm@sha256:75a17dd6f00b277975715fc094c4a1570d512708de6bb4c5dc130814813ebfe4 AS base ARG TARGETARCH # set workdir From b0b64ecd7a64789e520386501b231f60f25f283f Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 6 May 2025 08:55:52 +0200 Subject: [PATCH 098/108] Upgrade pinned Python dependencies (#12583) Co-authored-by: LocalStack Bot --- requirements-base-runtime.txt | 2 +- requirements-basic.txt | 2 +- requirements-dev.txt | 6 +++--- requirements-runtime.txt | 2 +- requirements-test.txt | 4 ++-- requirements-typehint.txt | 18 +++++++++--------- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index d8fcc647bd99c..4caa10a3d5ef0 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -34,7 +34,7 @@ click==8.1.8 # via localstack-core (pyproject.toml) constantly==23.10.4 # via localstack-twisted -cryptography==44.0.2 +cryptography==44.0.3 # via # localstack-core (pyproject.toml) # pyopenssl diff --git a/requirements-basic.txt b/requirements-basic.txt index 20ea505e5ffce..71a4c39b516e3 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -16,7 +16,7 @@ charset-normalizer==3.4.2 # via requests click==8.1.8 # via localstack-core (pyproject.toml) -cryptography==44.0.2 +cryptography==44.0.3 # via localstack-core (pyproject.toml) dill==0.3.6 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 026f21d135944..13e600fd905cb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,7 +25,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.234 +aws-cdk-asset-awscli-v1==2.2.235 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib @@ -102,7 +102,7 @@ coveralls==4.0.1 # via localstack-core (pyproject.toml) crontab==1.0.4 # via localstack-core -cryptography==44.0.2 +cryptography==44.0.3 # via # joserfc # localstack-core @@ -485,7 +485,7 @@ urllib3==2.4.0 # opensearch-py # requests # responses -virtualenv==20.30.0 +virtualenv==20.31.1 # via pre-commit websocket-client==1.8.0 # via localstack-core diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 9b44aafa471c3..37cbf4908e40c 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -76,7 +76,7 @@ constantly==23.10.4 # via localstack-twisted crontab==1.0.4 # via localstack-core (pyproject.toml) -cryptography==44.0.2 +cryptography==44.0.3 # via # joserfc # localstack-core diff --git a/requirements-test.txt b/requirements-test.txt index 816cafabd5a10..eeb774517342f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -25,7 +25,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.234 +aws-cdk-asset-awscli-v1==2.2.235 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib @@ -96,7 +96,7 @@ coverage==7.8.0 # via localstack-core (pyproject.toml) crontab==1.0.4 # via localstack-core -cryptography==44.0.2 +cryptography==44.0.3 # via # joserfc # localstack-core diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 378c1be5a17c8..b194cae94ca68 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -25,7 +25,7 @@ attrs==25.3.0 # jsonschema # localstack-twisted # referencing -aws-cdk-asset-awscli-v1==2.2.234 +aws-cdk-asset-awscli-v1==2.2.235 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib @@ -49,7 +49,7 @@ boto3==1.38.8 # kclpy-ext # localstack-core # moto-ext -boto3-stubs==1.38.7 +boto3-stubs==1.38.9 # via localstack-core (pyproject.toml) botocore==1.38.8 # via @@ -59,7 +59,7 @@ botocore==1.38.8 # localstack-core # moto-ext # s3transfer -botocore-stubs==1.38.7 +botocore-stubs==1.38.9 # via boto3-stubs build==1.2.2.post1 # via @@ -106,7 +106,7 @@ coveralls==4.0.1 # via localstack-core crontab==1.0.4 # via localstack-core -cryptography==44.0.2 +cryptography==44.0.3 # via # joserfc # localstack-core @@ -324,11 +324,11 @@ mypy-boto3-dynamodb==1.38.4 # via boto3-stubs mypy-boto3-dynamodbstreams==1.38.0 # via boto3-stubs -mypy-boto3-ec2==1.38.6 +mypy-boto3-ec2==1.38.9 # via boto3-stubs mypy-boto3-ecr==1.38.6 # via boto3-stubs -mypy-boto3-ecs==1.38.3 +mypy-boto3-ecs==1.38.9 # via boto3-stubs mypy-boto3-efs==1.38.0 # via boto3-stubs @@ -370,7 +370,7 @@ mypy-boto3-iotwireless==1.38.0 # via boto3-stubs mypy-boto3-kafka==1.38.0 # via boto3-stubs -mypy-boto3-kinesis==1.38.5 +mypy-boto3-kinesis==1.38.8 # via boto3-stubs mypy-boto3-kinesisanalytics==1.38.0 # via boto3-stubs @@ -386,7 +386,7 @@ mypy-boto3-logs==1.38.6 # via boto3-stubs mypy-boto3-managedblockchain==1.38.0 # via boto3-stubs -mypy-boto3-mediaconvert==1.38.0 +mypy-boto3-mediaconvert==1.38.9 # via boto3-stubs mypy-boto3-mediastore==1.38.0 # via boto3-stubs @@ -803,7 +803,7 @@ urllib3==2.4.0 # opensearch-py # requests # responses -virtualenv==20.30.0 +virtualenv==20.31.1 # via pre-commit websocket-client==1.8.0 # via localstack-core From 7a2f96f5ea32e23593189b9e89f21164dda1fbca Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Tue, 6 May 2025 10:25:16 +0200 Subject: [PATCH 099/108] APIGW: fix selection pattern for AWS Lambda integration (#12580) --- .../handlers/integration_response.py | 2 +- .../apigateway/test_apigateway_lambda.py | 17 +++++++++++++++-- .../test_apigateway_lambda.snapshot.json | 10 +++++----- .../test_apigateway_lambda.validation.json | 2 +- .../lambda_/functions/lambda_select_pattern.py | 4 ++-- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_response.py b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_response.py index 7f6ae374afdac..02d09db8332c1 100644 --- a/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_response.py +++ b/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_response.py @@ -69,7 +69,7 @@ def __call__( # we first need to find the right IntegrationResponse based on their selection template, linked to the status # code of the Response if integration_type == IntegrationType.AWS and "lambda:path/" in integration["uri"]: - selection_value = self.parse_error_message_from_lambda(body) or str(status_code) + selection_value = self.parse_error_message_from_lambda(body) else: selection_value = str(status_code) diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.py b/tests/aws/services/apigateway/test_apigateway_lambda.py index b2b26e680b6cf..8aa53aaca9890 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.py +++ b/tests/aws/services/apigateway/test_apigateway_lambda.py @@ -832,6 +832,7 @@ def test_lambda_selection_patterns( resourceId=resource_id, httpMethod="GET", statusCode="200", + selectionPattern="", ) # 4xx aws_client.apigateway.put_integration_response( @@ -839,15 +840,27 @@ def test_lambda_selection_patterns( resourceId=resource_id, httpMethod="GET", statusCode="405", - selectionPattern=".*400.*", + selectionPattern=".*four hundred.*", ) + # 5xx aws_client.apigateway.put_integration_response( restApiId=api_id, resourceId=resource_id, httpMethod="GET", statusCode="502", - selectionPattern=".*5\\d\\d.*", + selectionPattern=".+", + ) + + # assert that this does not get matched even though it's the status code returned by the Lambda, showing that + # AWS does match on the status code for this specific integration + # https://docs.aws.amazon.com/apigateway/latest/api/API_IntegrationResponse.html + aws_client.apigateway.put_integration_response( + restApiId=api_id, + resourceId=resource_id, + httpMethod="GET", + statusCode="504", + selectionPattern="200", ) aws_client.apigateway.create_deployment(restApiId=api_id, stageName="dev") diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json index f91bd1cb104c2..6cdf03ea63e3f 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json @@ -1473,23 +1473,23 @@ } }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_selection_patterns": { - "recorded-date": "05-09-2023, 21:54:21", + "recorded-date": "05-05-2025, 14:10:11", "recorded-content": { "lambda-selection-pattern-200": "Pass", "lambda-selection-pattern-400": { - "errorMessage": "Error: Raising 400 from within the Lambda function", + "errorMessage": "Error: Raising four hundred from within the Lambda function", "errorType": "Exception", "requestId": "", "stackTrace": [ - " File \"/var/task/lambda_select_pattern.py\", line 7, in handler\n raise Exception(\"Error: Raising 400 from within the Lambda function\")\n" + " File \"/var/task/lambda_select_pattern.py\", line 7, in handler\n raise Exception(\"Error: Raising four hundred from within the Lambda function\")\n" ] }, "lambda-selection-pattern-500": { - "errorMessage": "Error: Raising 500 from within the Lambda function", + "errorMessage": "Error: Raising five hundred from within the Lambda function", "errorType": "Exception", "requestId": "", "stackTrace": [ - " File \"/var/task/lambda_select_pattern.py\", line 9, in handler\n raise Exception(\"Error: Raising 500 from within the Lambda function\")\n" + " File \"/var/task/lambda_select_pattern.py\", line 9, in handler\n raise Exception(\"Error: Raising five hundred from within the Lambda function\")\n" ] } } diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json index 8dcc1e29b6fc8..c2a311dd64e4e 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json @@ -30,7 +30,7 @@ "last_validated_date": "2024-05-31T19:17:51+00:00" }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_selection_patterns": { - "last_validated_date": "2023-09-05T19:54:21+00:00" + "last_validated_date": "2025-05-05T14:10:11+00:00" }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_put_integration_aws_proxy_uri": { "last_validated_date": "2025-03-03T12:58:39+00:00" diff --git a/tests/aws/services/lambda_/functions/lambda_select_pattern.py b/tests/aws/services/lambda_/functions/lambda_select_pattern.py index 73d78943a4cd5..12429f1990555 100644 --- a/tests/aws/services/lambda_/functions/lambda_select_pattern.py +++ b/tests/aws/services/lambda_/functions/lambda_select_pattern.py @@ -4,8 +4,8 @@ def handler(event, context): case "200": return "Pass" case "400": - raise Exception("Error: Raising 400 from within the Lambda function") + raise Exception("Error: Raising four hundred from within the Lambda function") case "500": - raise Exception("Error: Raising 500 from within the Lambda function") + raise Exception("Error: Raising five hundred from within the Lambda function") case _: return "Error Value in the json request should either be 400 or 500 to demonstrate" From 1b1d7cda8105dc07db408cfa910a81fda7f343f0 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Palma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 6 May 2025 13:00:08 +0200 Subject: [PATCH 100/108] Step Functions: Add Telemetry for SFN_MOCK_CONFIG Usage (#12584) --- localstack-core/localstack/runtime/analytics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/localstack-core/localstack/runtime/analytics.py b/localstack-core/localstack/runtime/analytics.py index 6882878dac2ac..2612ee8637bf9 100644 --- a/localstack-core/localstack/runtime/analytics.py +++ b/localstack-core/localstack/runtime/analytics.py @@ -85,6 +85,7 @@ "OUTBOUND_HTTP_PROXY", "OUTBOUND_HTTPS_PROXY", "S3_DIR", + "SFN_MOCK_CONFIG", "TMPDIR", ] From 5df40f1961b3bdf8e9d5e1a40c895a7ca642dad4 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Tue, 6 May 2025 16:39:03 +0200 Subject: [PATCH 101/108] Events: add classmethod to recreate services (#12566) --- .../services/events/api_destination.py | 17 ++++++++++++++ .../localstack/services/events/connection.py | 23 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/localstack-core/localstack/services/events/api_destination.py b/localstack-core/localstack/services/events/api_destination.py index a7fe116eaed21..0bb9f097ffb4b 100644 --- a/localstack-core/localstack/services/events/api_destination.py +++ b/localstack-core/localstack/services/events/api_destination.py @@ -64,6 +64,23 @@ def __init__( description, ) + @classmethod + def restore_from_api_destination_and_connection( + cls, api_destination: ApiDestination, connection: Connection + ): + api_destination_service = cls( + name=api_destination.name, + region=api_destination.region, + account_id=api_destination.account_id, + connection_arn=api_destination.connection_arn, + connection=connection, + invocation_endpoint=api_destination.invocation_endpoint, + http_method=api_destination.http_method, + invocation_rate_limit_per_second=api_destination.invocation_rate_limit_per_second, + ) + api_destination_service.api_destination = api_destination + return api_destination_service + @property def arn(self) -> Arn: return self.api_destination.arn diff --git a/localstack-core/localstack/services/events/connection.py b/localstack-core/localstack/services/events/connection.py index bb855c9203e0c..c2b72a2025328 100644 --- a/localstack-core/localstack/services/events/connection.py +++ b/localstack-core/localstack/services/events/connection.py @@ -32,12 +32,16 @@ def __init__( auth_parameters: CreateConnectionAuthRequestParameters, description: ConnectionDescription | None = None, invocation_connectivity_parameters: ConnectivityResourceParameters | None = None, + create_secret: bool = True, ): self._validate_input(name, authorization_type) state = self._get_initial_state(authorization_type) - secret_arn = self.create_connection_secret( - region, account_id, name, authorization_type, auth_parameters - ) + + secret_arn = None + if create_secret: + secret_arn = self.create_connection_secret( + region, account_id, name, authorization_type, auth_parameters + ) public_auth_parameters = self._get_public_parameters(authorization_type, auth_parameters) self.connection = Connection( @@ -52,6 +56,19 @@ def __init__( invocation_connectivity_parameters, ) + @classmethod + def restore_from_connection(cls, connection: Connection): + connection_service = cls( + connection.name, + connection.region, + connection.account_id, + connection.authorization_type, + connection.auth_parameters, + create_secret=False, + ) + connection_service.connection = connection + return connection_service + @property def arn(self) -> Arn: return self.connection.arn From 92aa7761e35fb465189d39f123b0f3c730385896 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 7 May 2025 11:38:13 +0200 Subject: [PATCH 102/108] APIGW: fix binaryMediaTypes when importing/updating REST APIs (#12586) --- .../services/apigateway/exporter.py | 22 ++- .../localstack/services/apigateway/helpers.py | 13 +- tests/aws/files/pets.json | 4 + .../apigateway/test_apigateway_basic.py | 1 - .../apigateway/test_apigateway_extended.py | 16 +- .../test_apigateway_extended.snapshot.json | 64 +++++--- .../test_apigateway_extended.validation.json | 8 +- .../apigateway/test_apigateway_import.py | 41 ++++- .../test_apigateway_import.snapshot.json | 154 +++++++++++++++++- .../test_apigateway_import.validation.json | 12 +- .../resources/test_apigateway.py | 58 ++++++- .../resources/test_apigateway.snapshot.json | 65 +++++++- .../resources/test_apigateway.validation.json | 7 +- tests/aws/templates/apigateway.json | 6 +- .../apigateway_integration_from_s3.yml | 3 + 15 files changed, 425 insertions(+), 49 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/exporter.py b/localstack-core/localstack/services/apigateway/exporter.py index 42614ab4def8f..0706e794c1651 100644 --- a/localstack-core/localstack/services/apigateway/exporter.py +++ b/localstack-core/localstack/services/apigateway/exporter.py @@ -190,7 +190,15 @@ def export( self._add_paths(spec, resources, with_extension) self._add_models(spec, models["items"], "#/definitions") - return getattr(spec, self.export_formats.get(export_format))() + response = getattr(spec, self.export_formats.get(export_format))() + if ( + with_extension + and isinstance(response, dict) + and (binary_media_types := rest_api.get("binaryMediaTypes")) is not None + ): + response[OpenAPIExt.BINARY_MEDIA_TYPES] = binary_media_types + + return response class _OpenApiOAS30Exporter(_BaseOpenApiExporter): @@ -298,8 +306,16 @@ def export( self._add_models(spec, models["items"], "#/components/schemas") response = getattr(spec, self.export_formats.get(export_format))() - if isinstance(response, dict) and "components" not in response: - response["components"] = {} + if isinstance(response, dict): + if "components" not in response: + response["components"] = {} + + if ( + with_extension + and (binary_media_types := rest_api.get("binaryMediaTypes")) is not None + ): + response[OpenAPIExt.BINARY_MEDIA_TYPES] = binary_media_types + return response diff --git a/localstack-core/localstack/services/apigateway/helpers.py b/localstack-core/localstack/services/apigateway/helpers.py index aeb6eed73073a..6cb103d50f637 100644 --- a/localstack-core/localstack/services/apigateway/helpers.py +++ b/localstack-core/localstack/services/apigateway/helpers.py @@ -492,8 +492,10 @@ def import_api_from_openapi_spec( region_name = context.region # TODO: - # 1. validate the "mode" property of the spec document, "merge" or "overwrite" + # 1. validate the "mode" property of the spec document, "merge" or "overwrite", and properly apply it + # for now, it only considers it for the binaryMediaTypes # 2. validate the document type, "swagger" or "openapi" + mode = request.get("mode", "merge") rest_api.version = ( str(version) if (version := resolved_schema.get("info", {}).get("version")) else None @@ -948,7 +950,14 @@ def create_method_resource(child, method, method_schema): get_or_create_path(base_path + path, base_path=base_path) # binary types - rest_api.binaryMediaTypes = resolved_schema.get(OpenAPIExt.BINARY_MEDIA_TYPES, []) + if mode == "merge": + existing_binary_media_types = rest_api.binaryMediaTypes or [] + else: + existing_binary_media_types = [] + + rest_api.binaryMediaTypes = existing_binary_media_types + resolved_schema.get( + OpenAPIExt.BINARY_MEDIA_TYPES, [] + ) policy = resolved_schema.get(OpenAPIExt.POLICY) if policy: diff --git a/tests/aws/files/pets.json b/tests/aws/files/pets.json index 1965dd545a253..0e4f769ea277c 100644 --- a/tests/aws/files/pets.json +++ b/tests/aws/files/pets.json @@ -7,6 +7,10 @@ "schemes": [ "https" ], + "x-amazon-apigateway-binary-media-types": [ + "image/png", + "image/jpg" + ], "paths": { "/pets": { "get": { diff --git a/tests/aws/services/apigateway/test_apigateway_basic.py b/tests/aws/services/apigateway/test_apigateway_basic.py index 949e22cacbcd0..ec03c2b1612bb 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.py +++ b/tests/aws/services/apigateway/test_apigateway_basic.py @@ -78,7 +78,6 @@ THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) TEST_SWAGGER_FILE_JSON = os.path.join(THIS_FOLDER, "../../files/swagger.json") TEST_SWAGGER_FILE_YAML = os.path.join(THIS_FOLDER, "../../files/swagger.yaml") -TEST_IMPORT_REST_API_FILE = os.path.join(THIS_FOLDER, "../../files/pets.json") TEST_IMPORT_MOCK_INTEGRATION = os.path.join(THIS_FOLDER, "../../files/openapi-mock.json") TEST_IMPORT_REST_API_ASYNC_LAMBDA = os.path.join(THIS_FOLDER, "../../files/api_definition.yaml") diff --git a/tests/aws/services/apigateway/test_apigateway_extended.py b/tests/aws/services/apigateway/test_apigateway_extended.py index 54a253fc8febe..c95965db241c1 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.py +++ b/tests/aws/services/apigateway/test_apigateway_extended.py @@ -43,7 +43,13 @@ def _create(**kwargs): [TEST_IMPORT_PETSTORE_SWAGGER, TEST_IMPORT_PETS], ids=["TEST_IMPORT_PETSTORE_SWAGGER", "TEST_IMPORT_PETS"], ) -@markers.snapshot.skip_snapshot_verify(paths=["$..body.host"]) +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..body.host", + # TODO: not returned by LS + "$..endpointConfiguration.ipAddressType", + ] +) def test_export_swagger_openapi(aws_client, snapshot, import_apigw, import_file, region_name): snapshot.add_transformer( [ @@ -82,7 +88,13 @@ def test_export_swagger_openapi(aws_client, snapshot, import_apigw, import_file, [TEST_IMPORT_PETSTORE_SWAGGER, TEST_IMPORT_PETS], ids=["TEST_IMPORT_PETSTORE_SWAGGER", "TEST_IMPORT_PETS"], ) -@markers.snapshot.skip_snapshot_verify(paths=["$..body.servers..url"]) +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..body.servers..url", + # TODO: not returned by LS + "$..endpointConfiguration.ipAddressType", + ] +) def test_export_oas30_openapi(aws_client, snapshot, import_apigw, region_name, import_file): snapshot.add_transformer( [ diff --git a/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json b/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json index efdbdcbccf8f0..76db5eff4a01b 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "recorded-date": "15-04-2024, 21:43:25", + "recorded-date": "06-05-2025, 18:20:26", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -8,6 +8,7 @@ "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -638,13 +639,18 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETS]": { - "recorded-date": "15-04-2024, 21:43:56", + "recorded-date": "06-05-2025, 18:21:08", "recorded-content": { "import-api": { "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/png", + "image/jpg" + ], "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -727,6 +733,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "http", "httpMethod": "GET", "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", "responses": { @@ -734,8 +741,7 @@ "statusCode": "200" } }, - "passthroughBehavior": "when_no_match", - "type": "http" + "passthroughBehavior": "when_no_match" } } }, @@ -755,6 +761,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "http", "httpMethod": "GET", "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{id}", "responses": { @@ -765,12 +772,15 @@ "requestParameters": { "integration.request.path.id": "method.request.path.petId" }, - "passthroughBehavior": "when_no_match", - "type": "http" + "passthroughBehavior": "when_no_match" } } } - } + }, + "x-amazon-apigateway-binary-media-types": [ + "image/png", + "image/jpg" + ] }, "contentDisposition": "attachment; filename=\"swagger_1.0.0.json\"", "contentType": "application/octet-stream", @@ -782,7 +792,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "recorded-date": "15-04-2024, 21:45:03", + "recorded-date": "06-05-2025, 18:34:11", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -790,6 +800,7 @@ "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -1140,6 +1151,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "http", "httpMethod": "GET", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets", "responses": { @@ -1154,8 +1166,7 @@ "integration.request.querystring.page": "method.request.querystring.page", "integration.request.querystring.type": "method.request.querystring.type" }, - "passthroughBehavior": "when_no_match", - "type": "http" + "passthroughBehavior": "when_no_match" } }, "post": { @@ -1190,6 +1201,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "http", "httpMethod": "POST", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets", "responses": { @@ -1200,8 +1212,7 @@ } } }, - "passthroughBehavior": "when_no_match", - "type": "http" + "passthroughBehavior": "when_no_match" } }, "options": { @@ -1235,6 +1246,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1248,8 +1260,7 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match", - "type": "mock" + "passthroughBehavior": "when_no_match" } } }, @@ -1286,6 +1297,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "http", "httpMethod": "GET", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets/{petId}", "responses": { @@ -1299,8 +1311,7 @@ "requestParameters": { "integration.request.path.petId": "method.request.path.petId" }, - "passthroughBehavior": "when_no_match", - "type": "http" + "passthroughBehavior": "when_no_match" } }, "options": { @@ -1344,6 +1355,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1357,8 +1369,7 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match", - "type": "mock" + "passthroughBehavior": "when_no_match" } } }, @@ -1378,6 +1389,7 @@ } }, "x-amazon-apigateway-integration": { + "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1392,8 +1404,7 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match", - "type": "mock" + "passthroughBehavior": "when_no_match" } } } @@ -1468,13 +1479,18 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETS]": { - "recorded-date": "15-04-2024, 21:45:07", + "recorded-date": "06-05-2025, 18:34:49", "recorded-content": { "import-api": { "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/png", + "image/jpg" + ], "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -1620,7 +1636,11 @@ } } }, - "components": {} + "components": {}, + "x-amazon-apigateway-binary-media-types": [ + "image/png", + "image/jpg" + ] }, "contentDisposition": "attachment; filename=\"oas30_1.0.0.json\"", "contentType": "application/octet-stream", diff --git a/tests/aws/services/apigateway/test_apigateway_extended.validation.json b/tests/aws/services/apigateway/test_apigateway_extended.validation.json index f4b5c141dd2c2..1486731f72d07 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_extended.validation.json @@ -6,15 +6,15 @@ "last_validated_date": "2024-10-10T18:54:41+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "last_validated_date": "2024-04-15T21:45:02+00:00" + "last_validated_date": "2025-05-06T18:34:11+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETS]": { - "last_validated_date": "2024-04-15T21:45:04+00:00" + "last_validated_date": "2025-05-06T18:34:17+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "last_validated_date": "2024-04-15T21:43:24+00:00" + "last_validated_date": "2025-05-06T18:20:25+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETS]": { - "last_validated_date": "2024-04-15T21:43:30+00:00" + "last_validated_date": "2025-05-06T18:20:36+00:00" } } diff --git a/tests/aws/services/apigateway/test_apigateway_import.py b/tests/aws/services/apigateway/test_apigateway_import.py index 30b437f5f8799..47599ae5ae4e4 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.py +++ b/tests/aws/services/apigateway/test_apigateway_import.py @@ -389,12 +389,13 @@ def test_import_and_validate_rest_api( "$.get-resources-swagger-json.items..resourceMethods.OPTIONS", "$.get-resources-no-base-path-swagger.items..resourceMethods.GET", "$.get-resources-no-base-path-swagger.items..resourceMethods.OPTIONS", + # TODO: not returned by LS + "$..endpointConfiguration.ipAddressType", ] ) def test_import_rest_apis_with_base_path_swagger( self, base_path_type, - create_rest_apigw, apigw_create_rest_api, import_apigw, aws_client, @@ -925,3 +926,41 @@ def test_import_with_integer_http_status_code( # this fixture will iterate over every resource and match its method, methodResponse, integration and # integrationResponse apigw_snapshot_imported_resources(rest_api_id=rest_api_id, resources=response) + + @markers.aws.validated + @pytest.mark.parametrize( + "put_mode", + ["merge", "overwrite"], + ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + # not yet implemented + "$..endpointConfiguration.ipAddressType", + # issue because we create a new API internally, so we recreate names and resources + "$..name", + "$..rootResourceId", + # not returned even if empty in LocalStack + "$.get-rest-api.tags", + ] + ) + def test_put_rest_api_mode_binary_media_types( + self, aws_client, apigw_create_rest_api, snapshot, put_mode + ): + base_api = apigw_create_rest_api(binaryMediaTypes=["image/heif"]) + rest_api_id = base_api["id"] + snapshot.match("create-rest-api", base_api) + + get_api = aws_client.apigateway.get_rest_api(restApiId=rest_api_id) + snapshot.match("get-rest-api", get_api) + + spec_file = load_file(TEST_IMPORT_REST_API_FILE) + put_api = aws_client.apigateway.put_rest_api( + restApiId=rest_api_id, + body=spec_file, + mode=put_mode, + ) + snapshot.match("put-api", put_api) + + if is_aws_cloud(): + # waiting before cleaning up to avoid TooManyRequests, as we create multiple REST APIs + time.sleep(15) diff --git a/tests/aws/services/apigateway/test_apigateway_import.snapshot.json b/tests/aws/services/apigateway/test_apigateway_import.snapshot.json index 3a19bd674145f..649fc5bed285b 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_import.snapshot.json @@ -1382,13 +1382,14 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[ignore]": { - "recorded-date": "15-04-2024, 21:33:04", + "recorded-date": "06-05-2025, 18:24:25", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -1765,13 +1766,14 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[prepend]": { - "recorded-date": "15-04-2024, 21:34:01", + "recorded-date": "06-05-2025, 18:25:39", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -2154,13 +2156,14 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[split]": { - "recorded-date": "15-04-2024, 21:34:50", + "recorded-date": "06-05-2025, 18:26:25", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "EDGE" ] @@ -5309,5 +5312,150 @@ } } } + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_put_rest_api_mode_binary_media_types[merge]": { + "recorded-date": "06-05-2025, 18:14:29", + "recorded-content": { + "create-rest-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/heif" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "get-rest-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/heif" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "tags": {}, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/heif", + "image/png", + "image/jpg" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "tags": {}, + "version": "1.0.0", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_put_rest_api_mode_binary_media_types[overwrite]": { + "recorded-date": "06-05-2025, 18:15:09", + "recorded-content": { + "create-rest-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/heif" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "get-rest-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/heif" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "tags": {}, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/png", + "image/jpg" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "tags": {}, + "version": "1.0.0", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/apigateway/test_apigateway_import.validation.json b/tests/aws/services/apigateway/test_apigateway_import.validation.json index f92baec36081c..63670ed857343 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_import.validation.json @@ -18,13 +18,13 @@ "last_validated_date": "2024-12-12T22:45:20+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[ignore]": { - "last_validated_date": "2024-04-15T21:32:25+00:00" + "last_validated_date": "2025-05-06T18:23:50+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[prepend]": { - "last_validated_date": "2024-04-15T21:33:49+00:00" + "last_validated_date": "2025-05-06T18:25:10+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[split]": { - "last_validated_date": "2024-04-15T21:34:46+00:00" + "last_validated_date": "2025-05-06T18:26:24+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_swagger_api": { "last_validated_date": "2024-04-15T21:30:39+00:00" @@ -49,5 +49,11 @@ }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": { "last_validated_date": "2024-08-12T13:42:13+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_put_rest_api_mode_binary_media_types[merge]": { + "last_validated_date": "2025-05-06T18:14:28+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_put_rest_api_mode_binary_media_types[overwrite]": { + "last_validated_date": "2025-05-06T18:14:45+00:00" } } diff --git a/tests/aws/services/cloudformation/resources/test_apigateway.py b/tests/aws/services/cloudformation/resources/test_apigateway.py index b5c33580aed1e..bdae534baf3c6 100644 --- a/tests/aws/services/cloudformation/resources/test_apigateway.py +++ b/tests/aws/services/cloudformation/resources/test_apigateway.py @@ -3,14 +3,17 @@ from operator import itemgetter import requests +from localstack_snapshot.snapshots.transformer import SortingTransformer from localstack import constants from localstack.aws.api.lambda_ import Runtime +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.common import short_uid from localstack.utils.files import load_file from localstack.utils.run import to_str from localstack.utils.strings import to_bytes +from localstack.utils.sync import retry from tests.aws.services.apigateway.apigateway_fixtures import api_invoke_url PARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -108,7 +111,24 @@ def test_cfn_apigateway_aws_integration(deploy_cfn_template, aws_client): @markers.aws.validated -def test_cfn_apigateway_swagger_import(deploy_cfn_template, echo_http_server_post, aws_client): +@markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: not returned by LS + "$..endpointConfiguration.ipAddressType", + ] +) +def test_cfn_apigateway_swagger_import( + deploy_cfn_template, echo_http_server_post, aws_client, snapshot +): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("aws:cloudformation:stack-name"), + snapshot.transform.resource_name(), + snapshot.transform.key_value("id"), + snapshot.transform.key_value("name"), + snapshot.transform.key_value("rootResourceId"), + ] + ) api_name = f"rest-api-{short_uid()}" deploy_cfn_template( template=TEST_TEMPLATE_1, @@ -121,13 +141,25 @@ def test_cfn_apigateway_swagger_import(deploy_cfn_template, echo_http_server_pos ] assert len(apis) == 1 api_id = apis[0]["id"] + snapshot.match("imported-api", apis[0]) # construct API endpoint URL url = api_invoke_url(api_id, stage="dev", path="/test") # invoke API endpoint, assert results - result = requests.post(url, data="test 123") - assert result.ok + def _invoke(): + _result = requests.post(url, data="test 123") + assert _result.ok + return _result + + if is_aws_cloud(): + sleep = 2 + retries = 20 + else: + sleep = 0.1 + retries = 3 + + result = retry(_invoke, sleep=sleep, retries=retries) content = json.loads(to_str(result.content)) assert content["data"] == "test 123" assert content["url"].endswith("/post") @@ -301,12 +333,16 @@ def test_cfn_deploy_apigateway_integration(deploy_cfn_template, snapshot, aws_cl "$.get-stage.lastUpdatedDate", "$.get-stage.methodSettings", "$.get-stage.tags", + "$..endpointConfiguration.ipAddressType", ] ) def test_cfn_deploy_apigateway_from_s3_swagger( deploy_cfn_template, snapshot, aws_client, s3_bucket ): snapshot.add_transformer(snapshot.transform.key_value("deploymentId")) + # FIXME: we need to sort the binaryMediaTypes as we don't return it in the same order as AWS, but this does not have + # behavior incidence + snapshot.add_transformer(SortingTransformer("binaryMediaTypes")) # put the swagger file in S3 swagger_template = load_file( os.path.join(os.path.dirname(__file__), "../../../files/pets.json") @@ -344,7 +380,20 @@ def test_cfn_deploy_apigateway_from_s3_swagger( @markers.aws.validated -def test_cfn_apigateway_rest_api(deploy_cfn_template, aws_client): +@markers.snapshot.skip_snapshot_verify( + paths=["$..endpointConfiguration.ipAddressType"], +) +def test_cfn_apigateway_rest_api(deploy_cfn_template, aws_client, snapshot): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("aws:cloudformation:logical-id"), + snapshot.transform.key_value("aws:cloudformation:stack-name"), + snapshot.transform.resource_name(), + snapshot.transform.key_value("id"), + snapshot.transform.key_value("rootResourceId"), + ] + ) + stack = deploy_cfn_template( template_path=os.path.join(os.path.dirname(__file__), "../../../templates/apigateway.json") ) @@ -362,6 +411,7 @@ def test_cfn_apigateway_rest_api(deploy_cfn_template, aws_client): rs = aws_client.apigateway.get_rest_apis() apis = [item for item in rs["items"] if item["name"] == "DemoApi_dev"] assert len(apis) == 1 + snapshot.match("rest-api", apis[0]) rs = aws_client.apigateway.get_models(restApiId=apis[0]["id"]) assert len(rs["items"]) == 3 diff --git a/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json b/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json index 84ff13f4d5db1..446cef02dea60 100644 --- a/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json @@ -107,13 +107,20 @@ } }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_from_s3_swagger": { - "recorded-date": "24-09-2024, 20:22:38", + "recorded-date": "06-05-2025, 18:31:54", "recorded-content": { "rest-api": { "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "application/pdf", + "image/gif", + "image/jpg", + "image/png" + ], "createdDate": "datetime", "disableExecuteApiEndpoint": false, "endpointConfiguration": { + "ipAddressType": "ipv4", "types": [ "REGIONAL" ] @@ -669,5 +676,61 @@ } } } + }, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_swagger_import": { + "recorded-date": "05-05-2025, 14:23:13", + "recorded-content": { + "imported-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "*/*" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "tags": { + "aws:cloudformation:logical-id": "Api", + "aws:cloudformation:stack-id": "arn::cloudformation::111111111111:stack//", + "aws:cloudformation:stack-name": "" + }, + "version": "1.0" + } + } + }, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_rest_api": { + "recorded-date": "05-05-2025, 14:50:14", + "recorded-content": { + "rest-api": { + "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "image/jpg", + "image/png" + ], + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "ipAddressType": "ipv4", + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "DemoApi_dev", + "rootResourceId": "", + "tags": { + "aws:cloudformation:logical-id": "", + "aws:cloudformation:stack-id": "arn::cloudformation::111111111111:stack//", + "aws:cloudformation:stack-name": "" + } + } + } } } diff --git a/tests/aws/services/cloudformation/resources/test_apigateway.validation.json b/tests/aws/services/cloudformation/resources/test_apigateway.validation.json index e19c16876c071..4fb5cf01a3874 100644 --- a/tests/aws/services/cloudformation/resources/test_apigateway.validation.json +++ b/tests/aws/services/cloudformation/resources/test_apigateway.validation.json @@ -6,10 +6,13 @@ "last_validated_date": "2024-04-15T22:59:53+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_rest_api": { - "last_validated_date": "2024-06-25T18:12:55+00:00" + "last_validated_date": "2025-05-05T14:50:14+00:00" + }, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_swagger_import": { + "last_validated_date": "2025-05-05T14:23:13+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_from_s3_swagger": { - "last_validated_date": "2024-09-24T20:22:37+00:00" + "last_validated_date": "2025-05-06T18:31:53+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_integration": { "last_validated_date": "2024-02-21T12:54:34+00:00" diff --git a/tests/aws/templates/apigateway.json b/tests/aws/templates/apigateway.json index a8bf342d5886c..5b15fa054e39d 100644 --- a/tests/aws/templates/apigateway.json +++ b/tests/aws/templates/apigateway.json @@ -54,7 +54,11 @@ } ] ] - } + }, + "BinaryMediaTypes": [ + "image/jpg", + "image/png" + ] }, "Metadata": { "AWS::CloudFormation::Designer": { diff --git a/tests/aws/templates/apigateway_integration_from_s3.yml b/tests/aws/templates/apigateway_integration_from_s3.yml index ca6d6bf9f6da7..e8a6ef7c42963 100644 --- a/tests/aws/templates/apigateway_integration_from_s3.yml +++ b/tests/aws/templates/apigateway_integration_from_s3.yml @@ -12,6 +12,9 @@ Resources: ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: + BinaryMediaTypes: + - "image/gif" + - "application/pdf" BodyS3Location: Bucket: Ref: S3BodyBucket From 90b07b1e8fe3deb839199e480a74f8dac9c94c51 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Wed, 7 May 2025 17:50:25 +0530 Subject: [PATCH 103/108] Migrate MA/MR pipeline from CircleCI to GH Actions (#12579) --- .github/workflows/aws-tests-mamr.yml | 60 ++++++++++++++++++++++++++++ .github/workflows/aws-tests.yml | 45 ++++++++++++++++++--- 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/aws-tests-mamr.yml diff --git a/.github/workflows/aws-tests-mamr.yml b/.github/workflows/aws-tests-mamr.yml new file mode 100644 index 0000000000000..51fb472321aa7 --- /dev/null +++ b/.github/workflows/aws-tests-mamr.yml @@ -0,0 +1,60 @@ +name: AWS / MA/MR tests + +on: + schedule: + - cron: 0 1 * * MON-FRI + pull_request: + paths: + - '.github/workflows/aws-tests-mamr.yml' + - '.github/workflows/aws-tests.yml' + workflow_dispatch: + inputs: + disableCaching: + description: 'Disable Caching' + required: false + type: boolean + default: false + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING + +env: + IMAGE_NAME: "localstack/localstack" + TINYBIRD_DATASOURCE: "community_tests_circleci_ma_mr" + +jobs: + generate-random-creds: + name: "Generate random AWS credentials" + runs-on: ubuntu-latest + outputs: + region: ${{ steps.generate-aws-values.outputs.region }} + account_id: ${{ steps.generate-aws-values.outputs.account_id }} + steps: + - name: Generate values + id: generate-aws-values + run: | + # Generate a random 12-digit number for TEST_AWS_ACCOUNT_ID + ACCOUNT_ID=$(shuf -i 100000000000-999999999999 -n 1) + echo "account_id=$ACCOUNT_ID" >> $GITHUB_OUTPUT + # Set TEST_AWS_REGION_NAME to a random AWS region other than us-east-1 + REGIONS=("us-east-2" "us-west-1" "us-west-2" "ap-southeast-2" "ap-northeast-1" "eu-central-1" "eu-west-1") + REGION=${REGIONS[RANDOM % ${#REGIONS[@]}]} + echo "region=$REGION" >> $GITHUB_OUTPUT + + test-ma-mr: + name: "Run integration tests" + needs: generate-random-creds + uses: ./.github/workflows/aws-tests.yml + with: + disableCaching: ${{ inputs.disableCaching == true }} + PYTEST_LOGLEVEL: ${{ inputs.PYTEST_LOGLEVEL }} + testAWSRegion: ${{ needs.generate-random-creds.outputs.region }} + testAWSAccountId: ${{ needs.generate-random-creds.outputs.account_id }} + testAWSAccessKeyId: ${{ needs.generate-random-creds.outputs.account_id }} diff --git a/.github/workflows/aws-tests.yml b/.github/workflows/aws-tests.yml index f708a23799600..158d4005ad8e2 100644 --- a/.github/workflows/aws-tests.yml +++ b/.github/workflows/aws-tests.yml @@ -33,6 +33,21 @@ on: default: false required: false type: boolean + testAWSRegion: + description: 'AWS test region' + required: false + type: string + default: 'us-east-1' + testAWSAccountId: + description: 'AWS test account ID' + required: false + type: string + default: '000000000000' + testAWSAccessKeyId: + description: 'AWS test access key ID' + required: false + type: string + default: 'test' workflow_call: inputs: disableCaching: @@ -60,12 +75,30 @@ on: default: false required: false type: boolean + testAWSRegion: + description: 'AWS test region' + required: false + type: string + default: 'us-east-1' + testAWSAccountId: + description: 'AWS test account ID' + required: false + type: string + default: '000000000000' + testAWSAccessKeyId: + description: 'AWS test access key ID' + required: false + type: string + default: 'test' env: PYTEST_LOGLEVEL: ${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }} IMAGE_NAME: "localstack/localstack" TINYBIRD_DATASOURCE: "community_tests_integration" TESTSELECTION_PYTEST_ARGS: "${{ !inputs.disableTestSelection && '--path-filter=dist/testselection/test-selection.txt ' || '' }}" + TEST_AWS_REGION_NAME: ${{ inputs.testAWSRegion }} + TEST_AWS_ACCOUNT_ID: ${{ inputs.testAWSAccountId }} + TEST_AWS_ACCESS_KEY_ID: ${{ inputs.testAWSAccessKeyId }} jobs: build: @@ -139,7 +172,7 @@ jobs: fetch-depth: 0 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Linting run: make lint @@ -316,7 +349,7 @@ jobs: fetch-depth: 0 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Load Localstack Docker Image uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master @@ -437,7 +470,7 @@ jobs: uses: actions/checkout@v4 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Run Cloudwatch v1 Provider Tests timeout-minutes: 30 @@ -480,7 +513,7 @@ jobs: uses: actions/checkout@v4 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Download Test Selection if: ${{ env.TESTSELECTION_PYTEST_ARGS }} @@ -529,7 +562,7 @@ jobs: uses: actions/checkout@v4 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Download Test Selection if: ${{ env.TESTSELECTION_PYTEST_ARGS }} @@ -580,7 +613,7 @@ jobs: uses: actions/checkout@v4 - name: Prepare Local Test Environment - uses: localstack/localstack/.github/actions/setup-test-env@master + uses: localstack/localstack/.github/actions/setup-tests-env@master - name: Download Test Selection if: ${{ env.TESTSELECTION_PYTEST_ARGS }} From 549fd9b66127355b6a137753450f1de2163d7f3e Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Wed, 7 May 2025 15:52:03 +0200 Subject: [PATCH 104/108] Added main workflow to trigger full-run workflow and push results to docker registry and pypi (#12570) --- .../action.yml | 2 + .github/workflows/aws-main.yml | 170 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .github/workflows/aws-main.yml diff --git a/.github/actions/load-localstack-docker-from-artifacts/action.yml b/.github/actions/load-localstack-docker-from-artifacts/action.yml index e1928cd0433b3..97215dedb1042 100644 --- a/.github/actions/load-localstack-docker-from-artifacts/action.yml +++ b/.github/actions/load-localstack-docker-from-artifacts/action.yml @@ -26,4 +26,6 @@ runs: - name: Load Docker Image shell: bash + env: + PLATFORM: ${{ inputs.platform }} run: bin/docker-helper.sh load diff --git a/.github/workflows/aws-main.yml b/.github/workflows/aws-main.yml new file mode 100644 index 0000000000000..9a8ffecbea409 --- /dev/null +++ b/.github/workflows/aws-main.yml @@ -0,0 +1,170 @@ +name: AWS / Build, Test, Push + +on: + schedule: + - cron: 0 2 * * MON-FRI + push: + paths: + - '**' + - '.github/actions/**' + - '.github/workflows/aws-main.yml' + - '.github/workflows/aws-tests.yml' + - '!CODEOWNERS' + - '!README.md' + - '!.gitignore' + - '!.git-blame-ignore-revs' + - '!.github/**' + branches: + - master + workflow_dispatch: + inputs: + onlyAcceptanceTests: + description: 'Only run acceptance tests' + required: false + type: boolean + default: false + enableTestSelection: + description: 'Enable Test Selection' + required: false + type: boolean + default: false + disableCaching: + description: 'Disable Caching' + required: false + type: boolean + default: false + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING + +env: + # Docker Image name and default tag used by docker-helper.sh + IMAGE_NAME: "localstack/localstack" + DEFAULT_TAG: "latest" + PLATFORM_NAME_AMD64: "amd64" + PLATFORM_NAME_ARM64: "arm64" + +jobs: + test: + name: "Run integration tests" + uses: ./.github/workflows/aws-tests.yml + with: + # onlyAcceptance test is either explicitly set, or it's a push event. + # otherwise it's false (schedule event, workflow_dispatch event without setting it to true) + onlyAcceptanceTests: ${{ inputs.onlyAcceptanceTests == true || github.event_name == 'push' }} + # default "disableCaching" to `false` if it's a push or schedule event + disableCaching: ${{ inputs.disableCaching == true }} + # default "disableTestSelection" to `true` if it's a push or schedule event + disableTestSelection: ${{ inputs.enableTestSelection != true }} + PYTEST_LOGLEVEL: ${{ inputs.PYTEST_LOGLEVEL }} + secrets: + DOCKERHUB_PULL_USERNAME: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + DOCKERHUB_PULL_TOKEN: ${{ secrets.DOCKERHUB_PULL_TOKEN }} + + push: + name: "Push Images" + runs-on: ubuntu-latest + # push image on master, target branch not set, and the dependent steps were either successful or skipped + # TO-DO: enable job after workflow in CircleCI is disabled + if: false +# if: github.ref == 'refs/heads/master' && !failure() && !cancelled() + needs: + # all tests need to be successful for the image to be pushed + - test + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # setuptools_scm requires the git history (at least until the last tag) to determine the version + fetch-depth: 0 + + - name: Load Localstack ${{ env.PLATFORM_NAME_AMD64 }} Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: ${{ env.PLATFORM_NAME_AMD64 }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Push ${{ env.PLATFORM_NAME_AMD64 }} Docker Image + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUSH_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUSH_TOKEN }} + PLATFORM: ${{ env.PLATFORM_NAME_AMD64 }} + run: | + # Push to Docker Hub + ./bin/docker-helper.sh push + # Push to Amazon Public ECR + TARGET_IMAGE_NAME="public.ecr.aws/localstack/localstack" ./bin/docker-helper.sh push + + - name: Load Localstack ${{ env.PLATFORM_NAME_ARM64 }} Docker Image + uses: localstack/localstack/.github/actions/load-localstack-docker-from-artifacts@master + with: + platform: ${{ env.PLATFORM_NAME_ARM64 }} + + - name: Push ${{ env.PLATFORM_NAME_ARM64 }} Docker Image + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUSH_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUSH_TOKEN }} + PLATFORM: ${{ env.PLATFORM_NAME_ARM64 }} + run: | + # Push to Docker Hub + ./bin/docker-helper.sh push + # Push to Amazon Public ECR + TARGET_IMAGE_NAME="public.ecr.aws/localstack/localstack" ./bin/docker-helper.sh push + + - name: Push Multi-Arch Manifest + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUSH_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUSH_TOKEN }} + run: | + # Push to Docker Hub + ./bin/docker-helper.sh push-manifests + # Push to Amazon Public ECR + IMAGE_NAME="public.ecr.aws/localstack/localstack" ./bin/docker-helper.sh push-manifests + + - name: Publish dev release + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUSH_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUSH_TOKEN }} + run: | + if git describe --exact-match --tags >/dev/null 2>&1; then + echo "not publishing a dev release as this is a tagged commit" + else + source .venv/bin/activate + make publish || echo "dev release failed (maybe it is already published)" + fi + + cleanup: + name: "Cleanup" + runs-on: ubuntu-latest + # only remove the image artifacts if the build was successful + # (this allows a re-build of failed jobs until for the time of the retention period) + if: success() + needs: push + steps: + - uses: geekyeggo/delete-artifact@v5 + with: + # delete the docker images shared within the jobs (storage on GitHub is expensive) + name: | + localstack-docker-image-* + lambda-common-* + failOnError: false + token: ${{ secrets.GITHUB_TOKEN }} From c09b347cf9499758f3127d217fb57492429dfaff Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 7 May 2025 17:05:07 +0200 Subject: [PATCH 105/108] Fix provisioned concurrency set on Lambda alias (#12592) --- .../lambda_/invocation/counting_service.py | 9 ++++ tests/aws/services/lambda_/test_lambda.py | 53 +++++++++++++++++++ .../lambda_/test_lambda.snapshot.json | 38 +++++++++++++ .../lambda_/test_lambda.validation.json | 3 ++ 4 files changed, 103 insertions(+) diff --git a/localstack-core/localstack/services/lambda_/invocation/counting_service.py b/localstack-core/localstack/services/lambda_/invocation/counting_service.py index 25d2ddf79f689..3c7024288a305 100644 --- a/localstack-core/localstack/services/lambda_/invocation/counting_service.py +++ b/localstack-core/localstack/services/lambda_/invocation/counting_service.py @@ -156,6 +156,15 @@ def get_invocation_lease( provisioned_concurrency_config = function.provisioned_concurrency_configs.get( function_version.id.qualifier ) + if not provisioned_concurrency_config: + # check if any aliases point to the current version, and check the provisioned concurrency config + # for them. There can be only one config for a version, not matter if defined on the alias or version itself. + for alias in function.aliases.values(): + if alias.function_version == function_version.id.qualifier: + provisioned_concurrency_config = ( + function.provisioned_concurrency_configs.get(alias.name) + ) + break if provisioned_concurrency_config: available_provisioned_concurrency = ( provisioned_concurrency_config.provisioned_concurrent_executions diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index 205d2e9c1f113..61827dbee334e 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -2539,6 +2539,59 @@ def test_provisioned_concurrency(self, create_lambda_function, snapshot, aws_cli result2 = json.load(invoke_result2["Payload"]) assert result2 == "on-demand" + @markers.aws.validated + def test_provisioned_concurrency_on_alias(self, create_lambda_function, snapshot, aws_client): + """ + Tests provisioned concurrency created and invoked using an alias + """ + # TODO add test that you cannot set provisioned concurrency on both alias and version it points to + # TODO can you set provisioned concurrency on multiple aliases pointing to the same function version? + min_concurrent_executions = 10 + 5 + check_concurrency_quota(aws_client, min_concurrent_executions) + + func_name = f"test_lambda_{short_uid()}" + alias_name = "live" + + create_lambda_function( + func_name=func_name, + handler_file=TEST_LAMBDA_INVOCATION_TYPE, + runtime=Runtime.python3_12, + client=aws_client.lambda_, + ) + + v1 = aws_client.lambda_.publish_version(FunctionName=func_name) + aws_client.lambda_.create_alias( + FunctionName=func_name, Name=alias_name, FunctionVersion=v1["Version"] + ) + + put_provisioned = aws_client.lambda_.put_provisioned_concurrency_config( + FunctionName=func_name, Qualifier=alias_name, ProvisionedConcurrentExecutions=5 + ) + snapshot.match("put_provisioned_5", put_provisioned) + + get_provisioned_prewait = aws_client.lambda_.get_provisioned_concurrency_config( + FunctionName=func_name, Qualifier=alias_name + ) + + snapshot.match("get_provisioned_prewait", get_provisioned_prewait) + assert wait_until(concurrency_update_done(aws_client.lambda_, func_name, alias_name)) + get_provisioned_postwait = aws_client.lambda_.get_provisioned_concurrency_config( + FunctionName=func_name, Qualifier=alias_name + ) + snapshot.match("get_provisioned_postwait", get_provisioned_postwait) + + invoke_result1 = aws_client.lambda_.invoke(FunctionName=func_name, Qualifier=alias_name) + result1 = json.load(invoke_result1["Payload"]) + assert result1 == "provisioned-concurrency" + + invoke_result1 = aws_client.lambda_.invoke(FunctionName=func_name, Qualifier=v1["Version"]) + result1 = json.load(invoke_result1["Payload"]) + assert result1 == "provisioned-concurrency" + + invoke_result2 = aws_client.lambda_.invoke(FunctionName=func_name, Qualifier="$LATEST") + result2 = json.load(invoke_result2["Payload"]) + assert result2 == "on-demand" + @markers.aws.validated def test_lambda_provisioned_concurrency_scheduling( self, snapshot, create_lambda_function, aws_client diff --git a/tests/aws/services/lambda_/test_lambda.snapshot.json b/tests/aws/services/lambda_/test_lambda.snapshot.json index 85733509934a2..fcdab9b602f0e 100644 --- a/tests/aws/services/lambda_/test_lambda.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda.snapshot.json @@ -4463,5 +4463,43 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency_on_alias": { + "recorded-date": "07-05-2025, 09:26:54", + "recorded-content": { + "put_provisioned_5": { + "AllocatedProvisionedConcurrentExecutions": 0, + "AvailableProvisionedConcurrentExecutions": 0, + "LastModified": "date", + "RequestedProvisionedConcurrentExecutions": 5, + "Status": "IN_PROGRESS", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "get_provisioned_prewait": { + "AllocatedProvisionedConcurrentExecutions": 0, + "AvailableProvisionedConcurrentExecutions": 0, + "LastModified": "date", + "RequestedProvisionedConcurrentExecutions": 5, + "Status": "IN_PROGRESS", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get_provisioned_postwait": { + "AllocatedProvisionedConcurrentExecutions": 5, + "AvailableProvisionedConcurrentExecutions": 5, + "LastModified": "date", + "RequestedProvisionedConcurrentExecutions": 5, + "Status": "READY", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json index adc3a699f0367..2d0c3705565c0 100644 --- a/tests/aws/services/lambda_/test_lambda.validation.json +++ b/tests/aws/services/lambda_/test_lambda.validation.json @@ -74,6 +74,9 @@ "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency": { "last_validated_date": "2024-04-08T17:04:20+00:00" }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency_on_alias": { + "last_validated_date": "2025-05-07T09:26:54+00:00" + }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency": { "last_validated_date": "2024-04-08T17:08:10+00:00" }, From fd727d533f66392fcc9ac7417657fe02e5a9e0e1 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 7 May 2025 17:05:42 +0200 Subject: [PATCH 106/108] Defer CDK imports in infra provisioning (#12591) --- localstack-core/localstack/packages/api.py | 2 +- tests/aws/conftest.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/packages/api.py b/localstack-core/localstack/packages/api.py index 2ad3280e2d71c..bcc8add9577c5 100644 --- a/localstack-core/localstack/packages/api.py +++ b/localstack-core/localstack/packages/api.py @@ -8,7 +8,7 @@ from threading import RLock from typing import Any, Callable, Generic, List, Optional, ParamSpec, TypeVar -from plux import Plugin, PluginManager, PluginSpec # type: ignore[import-untyped] +from plux import Plugin, PluginManager, PluginSpec # type: ignore from localstack import config diff --git a/tests/aws/conftest.py b/tests/aws/conftest.py index c5c5612b76cef..3292bc6523de5 100644 --- a/tests/aws/conftest.py +++ b/tests/aws/conftest.py @@ -8,7 +8,6 @@ from localstack import config as localstack_config from localstack import constants -from localstack.testing.scenario.provisioning import InfraProvisioner from localstack.testing.snapshots.transformer_utility import ( SNAPSHOT_BASIC_TRANSFORMER, SNAPSHOT_BASIC_TRANSFORMER_NEW, @@ -85,6 +84,9 @@ def cdk_template_path(): # Note: Don't move this into testing lib @pytest.fixture(scope="session") def infrastructure_setup(cdk_template_path, aws_client): + # Note: import needs to be local to avoid CDK import on every test run, which takes quite some time + from localstack.testing.scenario.provisioning import InfraProvisioner + def _infrastructure_setup( namespace: str, force_synth: Optional[bool] = False ) -> InfraProvisioner: From 1d398de1e69bd4d5588b9db3687a9eb715f0f6fe Mon Sep 17 00:00:00 2001 From: Anastasia Dusak <61540676+k-a-il@users.noreply.github.com> Date: Wed, 7 May 2025 17:59:10 +0200 Subject: [PATCH 107/108] Added secrets definition for docker pull action in aws-tests and aws-mamr tests (#12593) --- .github/workflows/aws-tests-mamr.yml | 3 +++ .github/workflows/aws-tests.yml | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/aws-tests-mamr.yml b/.github/workflows/aws-tests-mamr.yml index 51fb472321aa7..769bf355eaa6c 100644 --- a/.github/workflows/aws-tests-mamr.yml +++ b/.github/workflows/aws-tests-mamr.yml @@ -58,3 +58,6 @@ jobs: testAWSRegion: ${{ needs.generate-random-creds.outputs.region }} testAWSAccountId: ${{ needs.generate-random-creds.outputs.account_id }} testAWSAccessKeyId: ${{ needs.generate-random-creds.outputs.account_id }} + secrets: + DOCKERHUB_PULL_USERNAME: ${{ secrets.DOCKERHUB_PULL_USERNAME }} + DOCKERHUB_PULL_TOKEN: ${{ secrets.DOCKERHUB_PULL_TOKEN }} diff --git a/.github/workflows/aws-tests.yml b/.github/workflows/aws-tests.yml index 158d4005ad8e2..09b881e6328e7 100644 --- a/.github/workflows/aws-tests.yml +++ b/.github/workflows/aws-tests.yml @@ -90,6 +90,13 @@ on: required: false type: string default: 'test' + secrets: + DOCKERHUB_PULL_USERNAME: + description: 'A DockerHub username - Used to avoid rate limiting issues.' + required: true + DOCKERHUB_PULL_TOKEN: + description: 'A DockerHub token - Used to avoid rate limiting issues.' + required: true env: PYTEST_LOGLEVEL: ${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }} From 21f6e5fb29e7cc6cc0773c2c51269ff20a27f031 Mon Sep 17 00:00:00 2001 From: "localstack[bot]" Date: Thu, 8 May 2025 07:03:27 +0000 Subject: [PATCH 108/108] release version 4.4.0