|
4 | 4 | from typing import Any, Final, Optional, TypedDict |
5 | 5 |
|
6 | 6 | import boto3 |
| 7 | +from botocore.exceptions import ClientError |
7 | 8 | from samtranslator.translator.transform import transform as transform_sam |
8 | 9 |
|
| 10 | +from localstack.aws.connect import connect_to |
9 | 11 | from localstack.services.cloudformation.engine.policy_loader import create_policy_loader |
| 12 | +from localstack.services.cloudformation.engine.template_preparer import parse_template |
10 | 13 | from localstack.services.cloudformation.engine.transformers import ( |
11 | 14 | FailedTransformationException, |
12 | 15 | execute_macro, |
|
26 | 29 | ) |
27 | 30 | from localstack.services.cloudformation.stores import get_cloudformation_store |
28 | 31 | from localstack.services.cloudformation.v2.entities import ChangeSet |
| 32 | +from localstack.utils import testutil |
29 | 33 |
|
30 | 34 | LOG = logging.getLogger(__name__) |
31 | 35 |
|
32 | 36 | SERVERLESS_TRANSFORM = "AWS::Serverless-2016-10-31" |
33 | 37 | EXTENSIONS_TRANSFORM = "AWS::LanguageExtensions" |
34 | 38 | SECRETSMANAGER_TRANSFORM = "AWS::SecretsManager-2020-07-23" |
| 39 | +INCLUDE_TRANSFORM = "AWS::Include" |
35 | 40 |
|
36 | 41 |
|
37 | 42 | # TODO: evaluate the use of subtypes to represent and validate types of transforms |
@@ -135,6 +140,32 @@ def _apply_global_serverless_transformation( |
135 | 140 | if region_before is not None: |
136 | 141 | os.environ["AWS_DEFAULT_REGION"] = region_before |
137 | 142 |
|
| 143 | + @staticmethod |
| 144 | + def _apply_global_include( |
| 145 | + global_transform: GlobalTransform, template: dict, parameters: dict, account_id, region_name |
| 146 | + ) -> dict: |
| 147 | + location = global_transform.parameters.get("Location") |
| 148 | + if not location or not location.startswith("s3://"): |
| 149 | + raise FailedTransformationException( |
| 150 | + transformation=INCLUDE_TRANSFORM, |
| 151 | + message="Unexpected Location parameter for AWS::Include transformer: %s" % location, |
| 152 | + ) |
| 153 | + |
| 154 | + s3_client = connect_to(aws_access_key_id=account_id, region_name=region_name).s3 |
| 155 | + bucket, _, path = location.removeprefix("s3://").partition("/") |
| 156 | + try: |
| 157 | + content = testutil.download_s3_object(s3_client, bucket, path) |
| 158 | + except ClientError: |
| 159 | + raise FailedTransformationException( |
| 160 | + transformation=INCLUDE_TRANSFORM, |
| 161 | + message="Error downloading S3 object '%s/%s'" % (bucket, path), |
| 162 | + ) |
| 163 | + try: |
| 164 | + template_to_include = parse_template(content) |
| 165 | + except Exception as e: |
| 166 | + raise FailedTransformationException(transformation=INCLUDE_TRANSFORM, message=str(e)) |
| 167 | + return {**template, **template_to_include} |
| 168 | + |
138 | 169 | @staticmethod |
139 | 170 | def _apply_global_macro_transformation( |
140 | 171 | account_id: str, |
@@ -179,6 +210,14 @@ def _apply_global_transform( |
179 | 210 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html |
180 | 211 | LOG.warning("%s is not yet supported. Ignoring.", SECRETSMANAGER_TRANSFORM) |
181 | 212 | transformed_template = template |
| 213 | + elif transform_name == INCLUDE_TRANSFORM: |
| 214 | + transformed_template = self._apply_global_include( |
| 215 | + global_transform=global_transform, |
| 216 | + region_name=self._change_set.region_name, |
| 217 | + account_id=self._change_set.account_id, |
| 218 | + template=template, |
| 219 | + parameters=parameters, |
| 220 | + ) |
182 | 221 | else: |
183 | 222 | transformed_template = self._apply_global_macro_transformation( |
184 | 223 | account_id=self._change_set.account_id, |
|
0 commit comments