diff --git a/samcli/commands/_utils/template.py b/samcli/commands/_utils/template.py index a8e7b244d97b..c425c4afaa4c 100644 --- a/samcli/commands/_utils/template.py +++ b/samcli/commands/_utils/template.py @@ -10,7 +10,7 @@ from botocore.utils import set_value_from_jmespath from samcli.commands.exceptions import UserException -from samcli.lib.samlib.resource_metadata_normalizer import ASSET_PATH_METADATA_KEY +from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer, ASSET_PATH_METADATA_KEY from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.yamlhelper import yaml_parse, yaml_dump from samcli.lib.utils.resources import ( @@ -265,6 +265,7 @@ def get_template_parameters(template_file): Template Parameters as a dictionary """ template_dict = get_template_data(template_file=template_file) + ResourceMetadataNormalizer.normalize(template_dict, True) return template_dict.get("Parameters", dict()) diff --git a/samcli/commands/package/package_context.py b/samcli/commands/package/package_context.py index 6e33a4f85395..196940778344 100644 --- a/samcli/commands/package/package_context.py +++ b/samcli/commands/package/package_context.py @@ -14,7 +14,6 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. - import json import logging import os @@ -135,7 +134,14 @@ def run(self): raise PackageFailedError(template_file=self.template_file, ex=str(ex)) from ex def _export(self, template_path, use_json): - template = Template(template_path, os.getcwd(), self.uploaders, self.code_signer) + template = Template( + template_path, + os.getcwd(), + self.uploaders, + self.code_signer, + normalize_template=True, + normalize_parameters=True, + ) exported_template = template.export() if use_json: diff --git a/samcli/lib/package/artifact_exporter.py b/samcli/lib/package/artifact_exporter.py index 7b464b69cefa..8a37207cc376 100644 --- a/samcli/lib/package/artifact_exporter.py +++ b/samcli/lib/package/artifact_exporter.py @@ -20,6 +20,7 @@ from botocore.utils import set_value_from_jmespath +from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer from samcli.lib.utils.resources import ( AWS_SERVERLESS_FUNCTION, AWS_CLOUDFORMATION_STACK, @@ -78,7 +79,9 @@ def do_export(self, resource_id, resource_dict, parent_dir): property_name=self.PROPERTY_NAME, resource_id=resource_id, template_path=abs_template_path ) - exported_template_dict = Template(template_path, parent_dir, self.uploaders, self.code_signer).export() + exported_template_dict = Template( + template_path, parent_dir, self.uploaders, self.code_signer, normalize_template=True + ).export() exported_template_str = yaml_dump(exported_template_dict) @@ -127,6 +130,8 @@ def __init__( ), metadata_to_export=frozenset(METADATA_EXPORT_LIST), template_str: Optional[str] = None, + normalize_template: bool = False, + normalize_parameters: bool = False, ): """ Reads the template and makes it ready for export @@ -144,6 +149,8 @@ def __init__( self.template_dir = template_dir self.code_signer = code_signer self.template_dict = yaml_parse(template_str) + if normalize_template: + ResourceMetadataNormalizer.normalize(self.template_dict, normalize_parameters) self.resources_to_export = resources_to_export self.metadata_to_export = metadata_to_export self.uploaders = uploaders diff --git a/samcli/lib/package/ecr_uploader.py b/samcli/lib/package/ecr_uploader.py index c0041e256772..763c0fc87480 100644 --- a/samcli/lib/package/ecr_uploader.py +++ b/samcli/lib/package/ecr_uploader.py @@ -79,7 +79,9 @@ def upload(self, image, resource_name): _tag = tag_translation(image, docker_image_id=docker_img.id, gen_tag=self.tag) repository = ( - self.ecr_repo if not isinstance(self.ecr_repo_multi, dict) else self.ecr_repo_multi.get(resource_name) + self.ecr_repo + if not self.ecr_repo_multi or not isinstance(self.ecr_repo_multi, dict) + else self.ecr_repo_multi.get(resource_name) ) docker_img.tag(repository=repository, tag=_tag) diff --git a/samcli/lib/package/image_utils.py b/samcli/lib/package/image_utils.py index 9dd02259048a..ac86694a338d 100644 --- a/samcli/lib/package/image_utils.py +++ b/samcli/lib/package/image_utils.py @@ -44,6 +44,10 @@ def tag_translation(image, docker_image_id=None, gen_tag="latest"): # NOTE(sriram-mv): Checksum truncation Length is set to 12 _id = docker_image_id.split(":")[1][:SHA_CHECKSUM_TRUNCATION_LENGTH] - name, tag = image.split(":") + if ":" in image: + name, tag = image.split(":") + else: + name = image + tag = None _tag = tag if tag else gen_tag return f"{name}-{_id}-{_tag}" diff --git a/samcli/lib/samlib/resource_metadata_normalizer.py b/samcli/lib/samlib/resource_metadata_normalizer.py index 7e1c745cb05a..6070e64aac36 100644 --- a/samcli/lib/samlib/resource_metadata_normalizer.py +++ b/samcli/lib/samlib/resource_metadata_normalizer.py @@ -4,6 +4,10 @@ import logging from pathlib import Path +import json +import re + +from samcli.lib.iac.cdk.utils import is_cdk_project RESOURCES_KEY = "Resources" PROPERTIES_KEY = "Properties" @@ -23,12 +27,17 @@ ASSET_BUNDLED_METADATA_KEY = "aws:asset:is-bundled" SAM_METADATA_SKIP_BUILD_KEY = "SkipBuild" +# https://github.com/aws/aws-cdk/blob/b1ecd3d49d7ebf97a54a80d06779ef0f0b113c16/packages/%40aws-cdk/assert-internal/lib/canonicalize-assets.ts#L19 +CDK_ASSET_PARAMETER_PATTERN = re.compile( + "^AssetParameters[0-9a-fA-F]{64}(?:S3Bucket|S3VersionKey|ArtifactHash)[0-9a-fA-F]{8}$" +) + LOG = logging.getLogger(__name__) class ResourceMetadataNormalizer: @staticmethod - def normalize(template_dict): + def normalize(template_dict, normalize_parameters=False): """ Normalize all Resources in the template with the Metadata Key on the resource. @@ -67,6 +76,35 @@ def normalize(template_dict): }, ) + # This is a work around to allow the customer to use sam deploy or package commands without the need to provide + # values for the CDK auto generated asset parameters. The suggested solution is to let CDK add some metadata to + # the autogenerated parameters, so sam can skip them safely. + + # Normalizing CDK auto generated parameters. Search for parameters that meet these conditions: + # 1- parameter name matches pattern + # `AssetParameters[0-9a-f]{64}(?:S3Bucket|S3VersionKey|ArtifactHash)[0-9A-F]{8}` + # 2- parameter type is string + # 3- there is no reference to this parameter any where in the template resources + # 4- there is no default value for this parameter + # We set an empty string as default value for the matching parameters, so the customer can use sam deploy or + # package commands without providing values for the auto generated parameters, as these parameters are not used + # in SAM (sam set the resources paths directly, and does not depend on template parameters) + if normalize_parameters and is_cdk_project(template_dict): + resources_as_string = json.dumps(resources) + parameters = template_dict.get("Parameters", {}) + + default_value = " " + for parameter_name, parameter_value in parameters.items(): + parameter_name_match = CDK_ASSET_PARAMETER_PATTERN.match(parameter_name) + if ( + parameter_name_match + and "Default" not in parameter_value + and parameter_value.get("Type", "") == "String" + and f'"Ref": "{parameter_name}"' not in resources_as_string + ): + LOG.debug("set default value for parameter %s to '%s'", parameter_name, default_value) + parameter_value["Default"] = default_value + @staticmethod def _replace_property(property_key, property_value, resource, logical_id): """ diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index 0f4db7c7c2b4..4e40ec7d25a8 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -1,5 +1,4 @@ import os -from samcli.lib.bootstrap.companion_stack.data_types import CompanionStack import shutil import tempfile import time @@ -40,6 +39,7 @@ def setUpClass(cls): cls.docker_client.api.pull(repository=repo, tag=tag) cls.docker_client.api.tag(f"{repo}:{tag}", "emulation-python3.8", tag="latest") cls.docker_client.api.tag(f"{repo}:{tag}", "emulation-python3.8-2", tag="latest") + cls.docker_client.api.tag(f"{repo}:{tag}", "colorsrandomfunctionf61b9209", tag="latest") # setup signing profile arn & name cls.signing_profile_name = os.environ.get("AWS_SIGNING_PROFILE_NAME") @@ -70,7 +70,7 @@ def tearDown(self): cfn_client.delete_stack(StackName=stack_name) super().tearDown() - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_package_and_deploy_no_s3_bucket_all_args(self, template_file): template_path = self.test_data_path.joinpath(template_file) with tempfile.NamedTemporaryFile(delete=False) as output_template_file: @@ -119,7 +119,7 @@ def test_package_and_deploy_no_s3_bucket_all_args(self, template_file): deploy_process = run_command(deploy_command_list_execute) self.assertEqual(deploy_process.process.returncode, 0) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_no_package_and_deploy_with_s3_bucket_all_args(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -145,7 +145,13 @@ def test_no_package_and_deploy_with_s3_bucket_all_args(self, template_file): deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) - @parameterized.expand(["aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml"]) + @parameterized.expand( + [ + "aws-serverless-function-image.yaml", + "aws-lambda-function-image.yaml", + "cdk_v1_synthesized_template_image_functions.json", + ] + ) def test_no_package_and_deploy_with_s3_bucket_all_args_image_repository(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -173,7 +179,11 @@ def test_no_package_and_deploy_with_s3_bucket_all_args_image_repository(self, te self.assertEqual(deploy_process_execute.process.returncode, 0) @parameterized.expand( - [("Hello", "aws-serverless-function-image.yaml"), ("MyLambdaFunction", "aws-lambda-function-image.yaml")] + [ + ("Hello", "aws-serverless-function-image.yaml"), + ("MyLambdaFunction", "aws-lambda-function-image.yaml"), + ("ColorsRandomFunctionF61B9209", "cdk_v1_synthesized_template_image_functions.json"), + ] ) def test_no_package_and_deploy_with_s3_bucket_all_args_image_repositories(self, resource_id, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -201,7 +211,13 @@ def test_no_package_and_deploy_with_s3_bucket_all_args_image_repositories(self, deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) - @parameterized.expand(["aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml"]) + @parameterized.expand( + [ + "aws-serverless-function-image.yaml", + "aws-lambda-function-image.yaml", + "cdk_v1_synthesized_template_image_functions.json", + ] + ) def test_no_package_and_deploy_with_s3_bucket_all_args_resolve_image_repos(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -228,7 +244,7 @@ def test_no_package_and_deploy_with_s3_bucket_all_args_resolve_image_repos(self, deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_no_package_and_deploy_with_s3_bucket_and_no_confirm_changeset(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -256,7 +272,7 @@ def test_no_package_and_deploy_with_s3_bucket_and_no_confirm_changeset(self, tem deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_no_redeploy_on_same_built_artifacts(self, template_file): template_path = self.test_data_path.joinpath(template_file) # Build project @@ -293,7 +309,7 @@ def test_deploy_no_redeploy_on_same_built_artifacts(self, template_file): # Does not cause a re-deploy self.assertEqual(deploy_process_execute.process.returncode, 1) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_no_package_and_deploy_with_s3_bucket_all_args_confirm_changeset(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -319,7 +335,7 @@ def test_no_package_and_deploy_with_s3_bucket_all_args_confirm_changeset(self, t deploy_process_execute = run_command_with_input(deploy_command_list, "Y".encode()) self.assertEqual(deploy_process_execute.process.returncode, 0) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_without_s3_bucket(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -352,7 +368,7 @@ def test_deploy_without_s3_bucket(self, template_file): deploy_process_execute.stderr, ) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_without_stack_name(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -373,7 +389,7 @@ def test_deploy_without_stack_name(self, template_file): deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 2) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_without_capabilities(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -396,8 +412,7 @@ def test_deploy_without_capabilities(self, template_file): deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 1) - @parameterized.expand(["aws-serverless-function.yaml"]) - def test_deploy_without_template_file(self, template_file): + def test_deploy_without_template_file(self): stack_name = self._method_to_stack_name(self.id()) # Package and Deploy in one go without confirming change set. @@ -417,7 +432,7 @@ def test_deploy_without_template_file(self, template_file): # Error template file not specified self.assertEqual(deploy_process_execute.process.returncode, 1) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_with_s3_bucket_switch_region(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -474,7 +489,7 @@ def test_deploy_with_s3_bucket_switch_region(self, template_file): stderr, ) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_twice_with_no_fail_on_empty_changeset(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -513,7 +528,7 @@ def test_deploy_twice_with_no_fail_on_empty_changeset(self, template_file): stdout = deploy_process_execute.stdout.strip() self.assertIn(bytes(f"No changes to deploy. Stack {stack_name} is up to date", encoding="utf-8"), stdout) - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_deploy_twice_with_fail_on_empty_changeset(self, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -1171,8 +1186,8 @@ def test_deploy_update_failed_disable_rollback(self, template_file): deploy_process_execute = run_command(deploy_command_list) self.assertEqual(deploy_process_execute.process.returncode, 0) - def test_deploy_logs_warning_with_cdk_project(self): - template_file = "aws-serverless-function-cdk.yaml" + @parameterized.expand(["aws-serverless-function-cdk.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) + def test_deploy_logs_warning_with_cdk_project(self, template_file): template_path = self.test_data_path.joinpath(template_file) stack_name = self._method_to_stack_name(self.id()) diff --git a/tests/integration/package/test_package_command_image.py b/tests/integration/package/test_package_command_image.py index cedab01bd0ac..bbb3884f9f1a 100644 --- a/tests/integration/package/test_package_command_image.py +++ b/tests/integration/package/test_package_command_image.py @@ -1,5 +1,4 @@ import os -import re import tempfile from subprocess import Popen, PIPE, TimeoutExpired @@ -34,6 +33,7 @@ def setUpClass(cls): cls.docker_client.api.pull(repository=repo, tag=tag) cls.docker_client.api.tag(f"{repo}:{tag}", "emulation-python3.8", tag="latest") cls.docker_client.api.tag(f"{repo}:{tag}", "emulation-python3.8-2", tag="latest") + cls.docker_client.api.tag(f"{repo}:{tag}", "colorsrandomfunctionf61b9209", tag="latest") super(TestPackageImage, cls).setUpClass() @@ -43,7 +43,13 @@ def setUp(self): def tearDown(self): super(TestPackageImage, self).tearDown() - @parameterized.expand(["aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml"]) + @parameterized.expand( + [ + "aws-serverless-function-image.yaml", + "aws-lambda-function-image.yaml", + "cdk_v1_synthesized_template_image_functions.json", + ] + ) def test_package_template_without_image_repository(self, template_file): template_path = self.test_data_path.joinpath(template_file) command_list = self.get_command_list(template=template_path) @@ -64,6 +70,7 @@ def test_package_template_without_image_repository(self, template_file): "aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml", "aws-lambda-function-image-and-api.yaml", + "cdk_v1_synthesized_template_image_functions.json", ] ) def test_package_template_with_image_repository(self, template_file): @@ -82,7 +89,11 @@ def test_package_template_with_image_repository(self, template_file): self.assertIn(f"{self.ecr_repo_name}", process_stdout.decode("utf-8")) @parameterized.expand( - [("Hello", "aws-serverless-function-image.yaml"), ("MyLambdaFunction", "aws-lambda-function-image.yaml")] + [ + ("Hello", "aws-serverless-function-image.yaml"), + ("MyLambdaFunction", "aws-lambda-function-image.yaml"), + ("ColorsRandomFunctionF61B9209", "cdk_v1_synthesized_template_image_functions.json"), + ] ) def test_package_template_with_image_repositories(self, resource_id, template_file): template_path = self.test_data_path.joinpath(template_file) @@ -101,7 +112,13 @@ def test_package_template_with_image_repositories(self, resource_id, template_fi self.assertIn(f"{self.ecr_repo_name}", process_stdout.decode("utf-8")) self.assertEqual(0, process.returncode) - @parameterized.expand(["aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml"]) + @parameterized.expand( + [ + "aws-serverless-function-image.yaml", + "aws-lambda-function-image.yaml", + "cdk_v1_synthesized_template_image_functions.json", + ] + ) def test_package_template_with_non_ecr_repo_uri_image_repository(self, template_file): template_path = self.test_data_path.joinpath(template_file) command_list = self.get_command_list( @@ -119,7 +136,13 @@ def test_package_template_with_non_ecr_repo_uri_image_repository(self, template_ self.assertEqual(2, process.returncode) self.assertIn("Error: Invalid value for '--image-repository'", process_stderr.decode("utf-8")) - @parameterized.expand(["aws-serverless-function-image.yaml", "aws-lambda-function-image.yaml"]) + @parameterized.expand( + [ + "aws-serverless-function-image.yaml", + "aws-lambda-function-image.yaml", + "cdk_v1_synthesized_template_image_functions.json", + ] + ) def test_package_template_and_s3_bucket(self, template_file): template_path = self.test_data_path.joinpath(template_file) command_list = self.get_command_list(s3_bucket=self.s3_bucket, template=template_path) diff --git a/tests/integration/package/test_package_command_zip.py b/tests/integration/package/test_package_command_zip.py index 52ff539fbc93..2a1385b338a0 100644 --- a/tests/integration/package/test_package_command_zip.py +++ b/tests/integration/package/test_package_command_zip.py @@ -7,8 +7,7 @@ from unittest import skipIf from parameterized import parameterized, param -from samcli.lib.utils.hash import dir_checksum, file_checksum -from samcli.lib.warnings.sam_cli_warning import CodeDeployWarning +from samcli.lib.utils.hash import dir_checksum from .package_integ_base import PackageIntegBase from tests.testing_utils import RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI, RUN_BY_CANARY @@ -26,7 +25,7 @@ def setUp(self): def tearDown(self): super().tearDown() - @parameterized.expand(["aws-serverless-function.yaml"]) + @parameterized.expand(["aws-serverless-function.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) def test_package_template_flag(self, template_file): template_path = self.test_data_path.joinpath(template_file) command_list = self.get_command_list(s3_bucket=self.s3_bucket.name, template=template_path) @@ -43,6 +42,7 @@ def test_package_template_flag(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -93,6 +93,7 @@ def test_package_without_required_args(self): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -136,6 +137,7 @@ def test_package_with_prefix(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -191,6 +193,7 @@ def test_package_with_output_template_file(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -247,6 +250,7 @@ def test_package_with_json(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -305,6 +309,7 @@ def test_package_with_force_upload(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -361,6 +366,7 @@ def test_package_with_kms_key(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-serverless-httpapi.yaml", @@ -417,6 +423,7 @@ def test_package_with_metadata(self, template_file): @parameterized.expand( [ + "cdk_v1_synthesized_template_zip_functions.json", "aws-serverless-function.yaml", "aws-serverless-api.yaml", "aws-appsync-graphqlschema.yaml", @@ -572,8 +579,8 @@ def test_package_with_deep_nested_template(self): uploads = re.findall(r"\.template", process_stderr) self.assertEqual(len(uploads), 2) - def test_package_logs_warning_for_cdk_project(self): - template_file = "aws-serverless-function-cdk.yaml" + @parameterized.expand(["aws-serverless-function-cdk.yaml", "cdk_v1_synthesized_template_zip_functions.json"]) + def test_package_logs_warning_for_cdk_project(self, template_file): template_path = self.test_data_path.joinpath(template_file) command_list = self.get_command_list(s3_bucket=self.s3_bucket.name, template_file=template_path) diff --git a/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/Dockerfile b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/Dockerfile new file mode 100644 index 000000000000..170b1f1cca3e --- /dev/null +++ b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/Dockerfile @@ -0,0 +1,15 @@ +ARG BASE_RUNTIME + +FROM public.ecr.aws/lambda/python:$BASE_RUNTIME + +ARG FUNCTION_DIR="/var/task" + +RUN mkdir -p $FUNCTION_DIR + +COPY main.py $FUNCTION_DIR + +COPY __init__.py $FUNCTION_DIR + +COPY requirements.txt $FUNCTION_DIR + +RUN python -m pip install -r $FUNCTION_DIR/requirements.txt -t $FUNCTION_DIR \ No newline at end of file diff --git a/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/__init__.py b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/main.py b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/main.py new file mode 100644 index 000000000000..0311bf9f49b4 --- /dev/null +++ b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/main.py @@ -0,0 +1,19 @@ +import numpy + +# from cryptography.fernet import Fernet + + +def handler(event, context): + + # Try using some of the modules to make sure they work & don't crash the process + # print(Fernet.generate_key()) + + return {"pi": "{0:.2f}".format(numpy.pi)} + + +def first_function_handler(event, context): + return "Hello World" + + +def second_function_handler(event, context): + return "Hello Mars" diff --git a/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/requirements.txt b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/requirements.txt new file mode 100644 index 000000000000..141927094f94 --- /dev/null +++ b/tests/integration/testdata/package/asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456/requirements.txt @@ -0,0 +1 @@ +numpy<1.20.4 diff --git a/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/__init__.py b/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/main.py b/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/main.py new file mode 100644 index 000000000000..0311bf9f49b4 --- /dev/null +++ b/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/main.py @@ -0,0 +1,19 @@ +import numpy + +# from cryptography.fernet import Fernet + + +def handler(event, context): + + # Try using some of the modules to make sure they work & don't crash the process + # print(Fernet.generate_key()) + + return {"pi": "{0:.2f}".format(numpy.pi)} + + +def first_function_handler(event, context): + return "Hello World" + + +def second_function_handler(event, context): + return "Hello Mars" diff --git a/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/requirements.txt b/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/requirements.txt new file mode 100644 index 000000000000..141927094f94 --- /dev/null +++ b/tests/integration/testdata/package/asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837/requirements.txt @@ -0,0 +1 @@ +numpy<1.20.4 diff --git a/tests/integration/testdata/package/aws-serverless-function-cdk.yaml b/tests/integration/testdata/package/aws-serverless-function-cdk.yaml index bd9a8e6e437f..34216cce1687 100644 --- a/tests/integration/testdata/package/aws-serverless-function-cdk.yaml +++ b/tests/integration/testdata/package/aws-serverless-function-cdk.yaml @@ -12,6 +12,6 @@ Resources: Timeout: 600 Metadata: aws:cdk:path: ApiCorsIssueStack/Lambda/Resource - aws:asset:path: ./lambda_code + aws:asset:path: . aws:asset:is-bundled: false - aws:asset:property: Code \ No newline at end of file + aws:asset:property: CodeUri \ No newline at end of file diff --git a/tests/integration/testdata/package/aws-serverlessrepo-application.yaml b/tests/integration/testdata/package/aws-serverlessrepo-application.yaml index 7b03b69ff37e..e96d5835d240 100644 --- a/tests/integration/testdata/package/aws-serverlessrepo-application.yaml +++ b/tests/integration/testdata/package/aws-serverlessrepo-application.yaml @@ -2,6 +2,15 @@ AWSTemplateFormatVersion : '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Sample ServerlessRepo Application +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.handler + Runtime: python3.7 + CodeUri: . + Timeout: 600 + Metadata: AWS::ServerlessRepo::Application: Name: my-app diff --git a/tests/integration/testdata/package/cdk_v1_synthesized_template_image_functions.json b/tests/integration/testdata/package/cdk_v1_synthesized_template_image_functions.json new file mode 100644 index 000000000000..77f24ab33675 --- /dev/null +++ b/tests/integration/testdata/package/cdk_v1_synthesized_template_image_functions.json @@ -0,0 +1,292 @@ +{ + "Resources": { + "ColorsRandomFunctionServiceRole1DE8E7EB": { + "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" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/ColorsRandomFunction/ServiceRole/Resource" + } + }, + "ColorsRandomFunctionF61B9209": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ImageUri": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456" + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "ColorsRandomFunctionServiceRole1DE8E7EB", + "Arn" + ] + }, + "ImageConfig": { + "Command": [ + "main.handler" + ] + }, + "PackageType": "Image" + }, + "DependsOn": [ + "ColorsRandomFunctionServiceRole1DE8E7EB" + ], + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/ColorsRandomFunction/Resource", + "aws:asset:path": "asset.6598609927b272b36fdf01072092f9851ddcd1b41ba294f736ce77091f5cc456", + "aws:asset:dockerfile-path": "Dockerfile", + "aws:asset:docker-build-args": { + "BASE_RUNTIME": "3.7" + }, + "aws:asset:property": "Code.ImageUri" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/01OS24CMQw9C/uM6QjYF6iQuquGE7gZdxSGJJXtFKEodycBgbp6Xz+5h361gbfFO16ks+O8zDYyQT4q2tlsRUgrnVyYzD4GUU5Wzf4nfCGjJyVuYiCJiS01XlujUxdDMW0yn9F/jwj5kIJtduu8+Ee0M/Gnx4meXjEOPeQhnu9zDYuRVYftFYH7R1XDLtVT3aGQIcvP+N/go1kzGOg3itPI11YvxYQ4Epxk+devod/AenES5zpOQZ0nGB54A2I5QvsaAQAA" + }, + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/package/cdk_v1_synthesized_template_zip_functions.json b/tests/integration/testdata/package/cdk_v1_synthesized_template_zip_functions.json new file mode 100644 index 000000000000..f9960f41b3fb --- /dev/null +++ b/tests/integration/testdata/package/cdk_v1_synthesized_template_zip_functions.json @@ -0,0 +1,316 @@ +{ + "Resources": { + "RandomCitiesFunctionServiceRole4EFB1CF5": { + "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" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/RandomCitiesFunction/ServiceRole/Resource" + } + }, + "RandomCitiesFunction5C47A2B8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837S3Bucket9F6483DC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837S3VersionKey61C1B485" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837S3VersionKey61C1B485" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "RandomCitiesFunctionServiceRole4EFB1CF5", + "Arn" + ] + }, + "Handler": "main.handler", + "Runtime": "python3.7" + }, + "DependsOn": [ + "RandomCitiesFunctionServiceRole4EFB1CF5" + ], + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/RandomCitiesFunction/Resource", + "aws:asset:path": "asset.b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/01OS24CMQw9C/uM6QjYF6iQuquGE7gZdxSGJJXtFKEodycBgbp6Xz+5h361gbfFO16ks+O8zDYyQT4q2tlsRUgrnVyYzD4GUU5Wzf4nfCGjJyVuYiCJiS01XlujUxdDMW0yn9F/jwj5kIJtduu8+Ee0M/Gnx4meXjEOPeQhnu9zDYuRVYftFYH7R1XDLtVT3aGQIcvP+N/go1kzGOg3itPI11YvxYQ4Epxk+devod/AenES5zpOQZ0nGB54A2I5QvsaAQAA" + }, + "Metadata": { + "aws:cdk:path": "CDKV1SupportDemoStack/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Parameters": { + "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837S3Bucket9F6483DC": { + "Type": "String", + "Description": "S3 bucket for asset \"b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837\"" + }, + "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837S3VersionKey61C1B485": { + "Type": "String", + "Description": "S3 key for asset version \"b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837\"" + }, + "AssetParametersb998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837ArtifactHash13EC3BFB": { + "Type": "String", + "Description": "Artifact hash for asset \"b998895901bf33127f2c9dce715854f8b35aa73fb7eb5245ba9721580bbe5837\"" + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/unit/commands/_utils/test_template.py b/tests/unit/commands/_utils/test_template.py index ee2eef8bd05d..962914efc4c9 100644 --- a/tests/unit/commands/_utils/test_template.py +++ b/tests/unit/commands/_utils/test_template.py @@ -73,6 +73,57 @@ def test_must_read_file_and_get_parameters(self, pathlib_mock, yaml_parse_mock): m.assert_called_with(filename, "r", encoding="utf-8") yaml_parse_mock.assert_called_with(file_data) + @patch("samcli.commands._utils.template.yaml_parse") + @patch("samcli.commands._utils.template.pathlib") + def test_must_read_file_get_and_normalize_parameters(self, pathlib_mock, yaml_parse_mock): + filename = "filename" + file_data = "contents of the file" + parse_result = { + "Parameters": { + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3VersionKeyA3EB644B": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + }, + "Resources": { + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": {"Analytics": "v2:deflate64:H4s"}, + "Metadata": {"aws:cdk:path": "Stack/CDKMetadata/Default"}, + }, + "Function1": { + "Properties": {"Code": "some value"}, + "Metadata": { + "aws:asset:path": "new path", + "aws:asset:property": "Code", + "aws:asset:is-bundled": False, + }, + }, + }, + } + + pathlib_mock.Path.return_value.exists.return_value = True # Fake that the file exists + + m = mock_open(read_data=file_data) + yaml_parse_mock.return_value = parse_result + + with patch("samcli.commands._utils.template.open", m): + result = get_template_parameters(filename) + + self.assertEqual( + result, + { + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3VersionKeyA3EB644B": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + "Default": " ", + } + }, + ) + + m.assert_called_with(filename, "r", encoding="utf-8") + yaml_parse_mock.assert_called_with(file_data) + @parameterized.expand([param(ValueError()), param(yaml.YAMLError())]) @patch("samcli.commands._utils.template.yaml_parse") @patch("samcli.commands._utils.template.pathlib") diff --git a/tests/unit/commands/package/test_package_context.py b/tests/unit/commands/package/test_package_context.py index d25dafe778c7..69ed95ae96d0 100644 --- a/tests/unit/commands/package/test_package_context.py +++ b/tests/unit/commands/package/test_package_context.py @@ -7,6 +7,7 @@ from samcli.commands.package.package_context import PackageContext from samcli.commands.package.exceptions import PackageFailedError from samcli.lib.package.artifact_exporter import Template +from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer class TestPackageCommand(TestCase): @@ -33,6 +34,7 @@ def test_template_permissions_error(self, patched_boto): with self.assertRaises(PackageFailedError): self.package_command_context.run() + @patch.object(ResourceMetadataNormalizer, "normalize", MagicMock()) @patch.object(Template, "export", MagicMock(return_value={})) @patch("boto3.Session") def test_template_path_valid_with_output_template(self, patched_boto): @@ -55,6 +57,7 @@ def test_template_path_valid_with_output_template(self, patched_boto): ) package_command_context.run() + @patch.object(ResourceMetadataNormalizer, "normalize", MagicMock()) @patch.object(Template, "export", MagicMock(return_value={})) @patch("boto3.Session") def test_template_path_valid(self, patched_boto): @@ -76,6 +79,7 @@ def test_template_path_valid(self, patched_boto): ) package_command_context.run() + @patch.object(ResourceMetadataNormalizer, "normalize", MagicMock()) @patch.object(Template, "export", MagicMock(return_value={})) @patch("boto3.Session") def test_template_path_valid_no_json(self, patched_boto): diff --git a/tests/unit/lib/package/test_artifact_exporter.py b/tests/unit/lib/package/test_artifact_exporter.py index 339340097e9a..84b9efa301c8 100644 --- a/tests/unit/lib/package/test_artifact_exporter.py +++ b/tests/unit/lib/package/test_artifact_exporter.py @@ -862,7 +862,9 @@ def test_export_cloudformation_stack(self, TemplateMock): self.assertEqual(resource_dict[property_name], result_path_style_s3_url) - TemplateMock.assert_called_once_with(template_path, parent_dir, self.uploaders_mock, self.code_signer_mock) + TemplateMock.assert_called_once_with( + template_path, parent_dir, self.uploaders_mock, self.code_signer_mock, normalize_template=True + ) template_instance_mock.export.assert_called_once_with() self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None) @@ -955,7 +957,9 @@ def test_export_serverless_application(self, TemplateMock): self.assertEqual(resource_dict[property_name], result_path_style_s3_url) - TemplateMock.assert_called_once_with(template_path, parent_dir, self.uploaders_mock, self.code_signer_mock) + TemplateMock.assert_called_once_with( + template_path, parent_dir, self.uploaders_mock, self.code_signer_mock, normalize_template=True + ) template_instance_mock.export.assert_called_once_with() self.s3_uploader_mock.upload.assert_called_once_with(mock.ANY, mock.ANY) self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None) @@ -1123,6 +1127,139 @@ def test_template_export(self, yaml_parse_mock): resource_type2_class.assert_called_once_with(self.uploaders_mock, self.code_signer_mock) resource_type2_instance.export.assert_called_once_with("Resource2", mock.ANY, template_dir) + @patch("samcli.lib.package.artifact_exporter.yaml_parse") + def test_cdk_template_export(self, yaml_parse_mock): + parent_dir = os.path.sep + template_dir = os.path.join(parent_dir, "foo", "bar") + template_path = os.path.join(template_dir, "path") + template_str = self.example_yaml_template() + + resource_type1_class = Mock() + resource_type1_class.RESOURCE_TYPE = "AWS::Lambda::Function" + resource_type1_class.ARTIFACT_TYPE = ZIP + resource_type1_class.EXPORT_DESTINATION = Destination.S3 + resource_type1_instance = Mock() + resource_type1_class.return_value = resource_type1_instance + + resources_to_export = [resource_type1_class] + + template_dict = { + "Resources": { + "Resource1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket_name", + "S3Key": "key_name", + }, + }, + "Metadata": { + "aws:cdk:path": "Stack/Resource1/Resource", + "aws:asset:path": "/path/code", + "aws:asset:is-bundled": False, + "aws:asset:property": "Code", + }, + }, + } + } + + open_mock = mock.mock_open() + yaml_parse_mock.return_value = template_dict + + # Patch the file open method to return template string + with patch("samcli.lib.package.artifact_exporter.open", open_mock(read_data=template_str)) as open_mock: + template_exporter = Template( + template_path, + parent_dir, + self.uploaders_mock, + self.code_signer_mock, + resources_to_export, + normalize_template=True, + ) + exported_template = template_exporter.export() + self.assertEqual(exported_template, template_dict) + + open_mock.assert_called_once_with(make_abs_path(parent_dir, template_path), "r") + + self.assertEqual(1, yaml_parse_mock.call_count) + + resource_type1_class.assert_called_once_with(self.uploaders_mock, self.code_signer_mock) + expected_resource_properties = { + "Code": "/path/code", + } + resource_type1_instance.export.assert_called_once_with( + "Resource1", expected_resource_properties, template_dir + ) + + @patch("samcli.lib.package.artifact_exporter.yaml_parse") + def test_cdk_template_export_with_normalize_parameter(self, yaml_parse_mock): + parent_dir = os.path.sep + template_dir = os.path.join(parent_dir, "foo", "bar") + template_path = os.path.join(template_dir, "path") + template_str = self.example_yaml_template() + + resource_type1_class = Mock() + resource_type1_class.RESOURCE_TYPE = "AWS::Lambda::Function" + resource_type1_class.ARTIFACT_TYPE = ZIP + resource_type1_class.EXPORT_DESTINATION = Destination.S3 + resource_type1_instance = Mock() + resource_type1_class.return_value = resource_type1_instance + + resources_to_export = [resource_type1_class] + + template_dict = { + "Parameters": { + "AssetParameters123": {"Type": "String", "Description": 'S3 bucket for asset "12345432"'}, + }, + "Resources": { + "Resource1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket_name", + "S3Key": "key_name", + }, + }, + "Metadata": { + "aws:cdk:path": "Stack/Resource1/Resource", + "aws:asset:path": "/path/code", + "aws:asset:is-bundled": False, + "aws:asset:property": "Code", + }, + }, + }, + } + + open_mock = mock.mock_open() + yaml_parse_mock.return_value = template_dict + + # Patch the file open method to return template string + with patch("samcli.lib.package.artifact_exporter.open", open_mock(read_data=template_str)) as open_mock: + template_exporter = Template( + template_path, + parent_dir, + self.uploaders_mock, + self.code_signer_mock, + resources_to_export, + normalize_template=True, + normalize_parameters=True, + ) + exported_template = template_exporter.export() + template_dict["Parameters"]["AssetParameters123"]["Default"] = " " + self.assertEqual(exported_template, template_dict) + + open_mock.assert_called_once_with(make_abs_path(parent_dir, template_path), "r") + + self.assertEqual(1, yaml_parse_mock.call_count) + + resource_type1_class.assert_called_once_with(self.uploaders_mock, self.code_signer_mock) + expected_resource_properties = { + "Code": "/path/code", + } + resource_type1_instance.export.assert_called_once_with( + "Resource1", expected_resource_properties, template_dir + ) + @patch("samcli.lib.package.artifact_exporter.yaml_parse") def test_template_export_with_globals(self, yaml_parse_mock): parent_dir = os.path.sep diff --git a/tests/unit/lib/package/test_image_utils.py b/tests/unit/lib/package/test_image_utils.py index 0fff2dccb7ba..c2e089920893 100644 --- a/tests/unit/lib/package/test_image_utils.py +++ b/tests/unit/lib/package/test_image_utils.py @@ -12,6 +12,10 @@ def test_tag_translation_with_image_id(self): local_image = "helloworld:v1" self.assertEqual("helloworld-1234-v1", tag_translation(local_image, docker_image_id="sha256:1234")) + def test_tag_translation_with_image_id_and_no_tag(self): + local_image = "helloworld" + self.assertEqual("helloworld-1234-latest", tag_translation(local_image, docker_image_id="sha256:1234")) + @patch("samcli.lib.package.image_utils.docker") def test_tag_translation_without_image_id(self, mock_docker): mock_docker_client = MagicMock() diff --git a/tests/unit/lib/samlib/test_resource_metadata_normalizer.py b/tests/unit/lib/samlib/test_resource_metadata_normalizer.py index 320d800cf1d0..f6cf20ff97ab 100644 --- a/tests/unit/lib/samlib/test_resource_metadata_normalizer.py +++ b/tests/unit/lib/samlib/test_resource_metadata_normalizer.py @@ -187,3 +187,132 @@ def test_no_skip_build_metadata_for_bundled_assets_metadata_equals_false(self): ResourceMetadataNormalizer.normalize(template_data) self.assertIsNone(template_data["Resources"]["Function1"]["Metadata"].get("SkipBuild")) + + def test_no_cdk_template_parameters_should_not_be_normalized(self): + template_data = { + "Parameters": { + "AssetParameters123456543": {"Type": "String", "Description": 'S3 bucket for asset "12345432"'}, + }, + "Resources": { + "Function1": { + "Properties": {"Code": "some value"}, + "Metadata": { + "aws:asset:path": "new path", + "aws:asset:property": "Code", + "aws:asset:is-bundled": False, + }, + } + }, + } + + ResourceMetadataNormalizer.normalize(template_data, True) + + self.assertIsNone(template_data["Parameters"]["AssetParameters123456543"].get("Default")) + + def test_cdk_template_parameters_should_be_normalized(self): + template_data = { + "Parameters": { + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652998": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492C62394e8c406ab4004f0c80364BAB4957e67e31cf1130481ArtifactHash0A65c998": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3Bucket0A652998": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3VersionKey0A652998": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652999": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + "Default": "/path", + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652900": { + "Type": "notString", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652345": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652345123": { + "Type": "String", + "Description": 'S3 bucket for asset "12345432"', + }, + }, + "Resources": { + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": {"Analytics": "v2:deflate64:H4s"}, + "Metadata": {"aws:cdk:path": "Stack/CDKMetadata/Default"}, + }, + "Function1": { + "Properties": {"Code": "some value"}, + "Metadata": { + "aws:asset:path": "new path", + "aws:asset:property": "Code", + "aws:asset:is-bundled": False, + }, + }, + "Function2": { + "Properties": { + "Code": { + "Ref": "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652345" + } + }, + }, + }, + } + + ResourceMetadataNormalizer.normalize(template_data, True) + self.assertEqual( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652998" + ]["Default"], + " ", + ) + self.assertEqual( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492C62394e8c406ab4004f0c80364BAB4957e67e31cf1130481ArtifactHash0A65c998" + ]["Default"], + " ", + ) + self.assertEqual( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3Bucket0A652998" + ]["Default"], + " ", + ) + self.assertEqual( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481S3VersionKey0A652998" + ]["Default"], + " ", + ) + self.assertEqual( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652999" + ]["Default"], + "/path", + ) + self.assertIsNone( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652900" + ].get("Default") + ) + self.assertIsNone( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652345" + ].get("Default") + ) + self.assertIsNone( + template_data["Parameters"][ + "AssetParametersb9866fd422d32492c62394e8c406ab4004f0c80364bab4957e67e31cf1130481ArtifactHash0A652345123" + ].get("Default") + )