def to_statement(self, parameter_values): """ With the given values for each parameter, this method will return a policy statement that can be used directly with IAM. :param dict parameter_values: Dict containing values for each parameter defined in the template :return dict: Dictionary containing policy statement :raises InvalidParameterValues: If parameter values is not a valid dictionary or does not contain values for all parameters :raises InsufficientParameterValues: If the parameter values don't have values for all required parameters """ missing = self.missing_parameter_values(parameter_values) if len(missing) > 0: # str() of elements of list to prevent any `u` prefix from being displayed in user-facing error message raise InsufficientParameterValues( "Following required parameters of template '{}' don't have values: {}" .format(self.name, [str(m) for m in missing])) # Select only necessary parameter_values. this is to prevent malicious or accidental # injection of values for parameters not intended in the template. This is important because "Ref" resolution # will substitute any references for which a value is provided. necessary_parameter_values = { name: value for name, value in parameter_values.items() if name in self.parameters } # Only "Ref" is supported supported_intrinsics = {RefAction.intrinsic_name: RefAction()} resolver = IntrinsicsResolver(necessary_parameter_values, supported_intrinsics) definition_copy = copy.deepcopy(self.definition) return resolver.resolve_parameter_refs(definition_copy)
def to_statement(self, parameter_values): """ With the given values for each parameter, this method will return a policy statement that can be used directly with IAM. :param dict parameter_values: Dict containing values for each parameter defined in the template :return dict: Dictionary containing policy statement :raises InvalidParameterValues: If parameter values is not a valid dictionary or does not contain values for all parameters :raises InsufficientParameterValues: If the parameter values don't have values for all required parameters """ missing = self.missing_parameter_values(parameter_values) if len(missing) > 0: # str() of elements of list to prevent any `u` prefix from being displayed in user-facing error message raise InsufficientParameterValues("Following required parameters of template '{}' don't have values: {}" .format(self.name, [str(m) for m in missing])) # Select only necessary parameter_values. this is to prevent malicious or accidental # injection of values for parameters not intended in the template. This is important because "Ref" resolution # will substitute any references for which a value is provided. necessary_parameter_values = {name: value for name, value in parameter_values.items() if name in self.parameters} # Only "Ref" is supported supported_intrinsics = { RefAction.intrinsic_name: RefAction() } resolver = IntrinsicsResolver(necessary_parameter_values, supported_intrinsics) definition_copy = copy.deepcopy(self.definition) return resolver.resolve_parameter_refs(definition_copy)
def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_sam_resource_refs = Mock() # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_sam_resource_refs(input, {}), expected) resolver._try_resolve_sam_resource_refs.assert_not_called()
def setUp(self): self.parameter_values = { "param1": "value1", "param2": "value2", "param3": "value3" } self.resolver = IntrinsicsResolver(self.parameter_values)
def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_sam_resource_refs = Mock() # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_sam_resource_refs(input, {}), expected) resolver._try_resolve_sam_resource_refs.assert_not_called()
def _resolve_parameters(template_dict, parameter_overrides): """ In the given template, apply parameter values to resolve intrinsic functions Parameters ---------- template_dict : dict SAM Template parameter_overrides : dict Values for template parameters provided by user Returns ------- dict Resolved SAM template """ parameter_values = SamBaseProvider._get_parameter_values(template_dict, parameter_overrides) supported_intrinsics = {action.intrinsic_name: action() for action in SamBaseProvider._SUPPORTED_INTRINSICS} # Intrinsics resolver will mutate the original template return IntrinsicsResolver(parameters=parameter_values, supported_intrinsics=supported_intrinsics)\ .resolve_parameter_refs(template_dict)
class TestCodeUri(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": {"foo": "bar"}, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_code_uri(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, {"S3Key": "foo.zip", "S3Bucket": "foobar"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_zip_file(self): function = SamFunction("foo") function.InlineCode = "hello world" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, {"ZipFile": "hello world"}) def test_with_no_code_uri_or_zipfile(self): function = SamFunction("foo") with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs)
class TestApiTags(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": { "foo": "bar" }, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_no_tags(self): api = SamApi("foo") api.Tags = {} resources = api.to_cloudformation(**self.kwargs) deployment = [x for x in resources if isinstance(x, ApiGatewayStage)] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].Tags, []) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_tags(self): api = SamApi("foo") api.Tags = {"MyKey": "MyValue"} resources = api.to_cloudformation(**self.kwargs) deployment = [x for x in resources if isinstance(x, ApiGatewayStage)] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].Tags, [{ "Key": "MyKey", "Value": "MyValue" }])
class TestHttpApiDescription(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": { "foo": "bar" }, } @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_no_description(self): sam_http_api = SamHttpApi("foo") sam_http_api.DefinitionUri = "s3://foobar/foo.zip" resources = sam_http_api.to_cloudformation(**self.kwargs) rest_api = [x for x in resources if isinstance(x, ApiGatewayV2HttpApi)] self.assertEqual(rest_api[0].Description, None) @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_description(self): sam_http_api = SamHttpApi("foo") sam_http_api.DefinitionUri = "s3://foobar/foo.zip" sam_http_api.Description = "my description" resources = sam_http_api.to_cloudformation(**self.kwargs) rest_api = [x for x in resources if isinstance(x, ApiGatewayV2HttpApi)] self.assertEqual(rest_api[0].Description, "my description")
class TestVersionDescription(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": { "foo": "bar" }, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_version_description(self): function = SamFunction("foo") test_description = "foobar" function.Runtime = "foo" function.Handler = "bar" function.CodeUri = "s3://foobar/foo.zip" function.VersionDescription = test_description function.AutoPublishAlias = "live" cfnResources = function.to_cloudformation(**self.kwargs) generateFunctionVersion = [ x for x in cfnResources if isinstance(x, LambdaVersion) ] self.assertEqual(generateFunctionVersion[0].Description, test_description)
class TestApiTags(TestCase): kwargs = { 'intrinsics_resolver': IntrinsicsResolver({}), 'event_resources': [], 'managed_policy_map': { "foo": "bar" } } def test_with_no_tags(self): api = SamApi("foo") api.Tags = {} resources = api.to_cloudformation(**self.kwargs) deployment = [x for x in resources if isinstance(x, ApiGatewayStage)] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].Tags, []) def test_with_tags(self): api = SamApi("foo") api.Tags = {'MyKey': 'MyValue'} resources = api.to_cloudformation(**self.kwargs) deployment = [x for x in resources if isinstance(x, ApiGatewayStage)] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].Tags, [{ 'Key': 'MyKey', 'Value': 'MyValue' }])
def setUp(self): self.parameter_values = { "param1": "value1", "param2": "value2", "param3": "value3" } self.resolver = IntrinsicsResolver(self.parameter_values)
class TestResourceReferenceResolution(TestCase): def setUp(self): self.resolver = IntrinsicsResolver({}) @patch.object(IntrinsicsResolver, "_try_resolve_sam_resource_refs") @patch.object(IntrinsicsResolver, "_traverse") def test_resolve_sam_resource_refs(self, traverse_mock, try_resolve_mock): input = "foo" supported_refs = Mock() self.resolver.resolve_sam_resource_refs(input, supported_refs) traverse_mock.assert_called_once_with(input, supported_refs, try_resolve_mock) def test_resolve_sam_resource_refs_on_supported_intrinsic(self): action_mock = Mock() self.resolver.supported_intrinsics = {"foo": action_mock} input = {"foo": "bar"} supported_refs = Mock() self.resolver._try_resolve_sam_resource_refs(input, supported_refs) action_mock.resolve_resource_refs.assert_called_once_with( input, supported_refs) def test_resolve_sam_resource_refs_on_unknown_intrinsic(self): action_mock = Mock() self.resolver.supported_intrinsics = {"foo": action_mock} input = {"a": "b"} expected = {"a": "b"} supported_refs = Mock() result = self.resolver._try_resolve_sam_resource_refs( input, supported_refs) self.assertEqual(result, expected) action_mock.resolve_sam_resource_refs.assert_not_called() def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_sam_resource_refs = Mock( ) # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_sam_resource_refs(input, {}), expected) resolver._try_resolve_sam_resource_refs.assert_not_called()
class TestResourceReferenceResolution(TestCase): def setUp(self): self.resolver = IntrinsicsResolver({}) @patch.object(IntrinsicsResolver, "_try_resolve_sam_resource_refs") @patch.object(IntrinsicsResolver, "_traverse") def test_resolve_sam_resource_refs(self, traverse_mock, try_resolve_mock): input = "foo" supported_refs = Mock() self.resolver.resolve_sam_resource_refs(input, supported_refs) traverse_mock.assert_called_once_with(input, supported_refs, try_resolve_mock) def test_resolve_sam_resource_refs_on_supported_intrinsic(self): action_mock = Mock() self.resolver.supported_intrinsics = { "foo": action_mock } input = {"foo": "bar"} supported_refs = Mock() self.resolver._try_resolve_sam_resource_refs(input, supported_refs) action_mock.resolve_resource_refs.assert_called_once_with(input, supported_refs) def test_resolve_sam_resource_refs_on_unknown_intrinsic(self): action_mock = Mock() self.resolver.supported_intrinsics = { "foo": action_mock } input = {"a": "b"} expected = {"a": "b"} supported_refs = Mock() result = self.resolver._try_resolve_sam_resource_refs(input, supported_refs) self.assertEqual(result, expected) action_mock.resolve_sam_resource_refs.assert_not_called() def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_sam_resource_refs = Mock() # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_sam_resource_refs(input, {}), expected) resolver._try_resolve_sam_resource_refs.assert_not_called()
def test_configure_supported_intrinsics_must_error_for_non_action_value(self): class SomeAction(Action): intrinsic_name = "Foo" # All intrinsics must have a value to be subclass of "Action" supported_intrinsics = {"A": "B", "Foo": SomeAction()} with self.assertRaises(TypeError): IntrinsicsResolver({}, supported_intrinsics)
def test_by_default_all_intrinsics_must_be_supported(self): # Just make sure we never remove support for some intrinsic resolver = IntrinsicsResolver({}) # This needs to be updated when new intrinsics are added self.assertEqual(3, len(resolver.supported_intrinsics)) self.assertTrue("Ref" in resolver.supported_intrinsics) self.assertTrue("Fn::Sub" in resolver.supported_intrinsics) self.assertTrue("Fn::GetAtt" in resolver.supported_intrinsics)
def test_configure_supported_intrinsics(self): class SomeAction(Action): intrinsic_name = "IntrinsicName" action = SomeAction() supported_intrinsics = {"ThisCanBeAnyIntrinsicName": action} resolver = IntrinsicsResolver({}, supported_intrinsics) self.assertEqual(resolver.supported_intrinsics, {"ThisCanBeAnyIntrinsicName": action})
class TestOpenApi(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": { "foo": "bar" }, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_open_api_3_no_stage(self): api = SamApi("foo") api.OpenApiVersion = "3.0" resources = api.to_cloudformation(**self.kwargs) deployment = [ x for x in resources if isinstance(x, ApiGatewayDeployment) ] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].StageName, None) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_open_api_2_no_stage(self): api = SamApi("foo") api.OpenApiVersion = "3.0" resources = api.to_cloudformation(**self.kwargs) deployment = [ x for x in resources if isinstance(x, ApiGatewayDeployment) ] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].StageName, None) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_open_api_bad_value(self): api = SamApi("foo") api.OpenApiVersion = "5.0" with pytest.raises(InvalidResourceException): api.to_cloudformation(**self.kwargs) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_swagger_no_stage(self): api = SamApi("foo") resources = api.to_cloudformation(**self.kwargs) deployment = [ x for x in resources if isinstance(x, ApiGatewayDeployment) ] self.assertEqual(deployment.__len__(), 1) self.assertEqual(deployment[0].StageName, "Stage")
class TestAssumeRolePolicyDocument(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": {"foo": "bar"}, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_assume_role_policy_document(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" assume_role_policy_document = { "Version": "2012-10-17", "Statement": [ { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]}, } ], } function.AssumeRolePolicyDocument = assume_role_policy_document cfnResources = function.to_cloudformation(**self.kwargs) generateFunctionVersion = [x for x in cfnResources if isinstance(x, IAMRole)] self.assertEqual(generateFunctionVersion[0].AssumeRolePolicyDocument, assume_role_policy_document) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_without_assume_role_policy_document(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" assume_role_policy_document = { "Version": "2012-10-17", "Statement": [ {"Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}} ], } cfnResources = function.to_cloudformation(**self.kwargs) generateFunctionVersion = [x for x in cfnResources if isinstance(x, IAMRole)] self.assertEqual(generateFunctionVersion[0].AssumeRolePolicyDocument, assume_role_policy_document)
class TestCodeUri(TestCase): kwargs = { 'intrinsics_resolver': IntrinsicsResolver({}), 'event_resources': [], 'managed_policy_map': { "foo": "bar" } } def test_with_code_uri(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [ x for x in cfnResources if isinstance(x, LambdaFunction) ] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, { "S3Key": "foo.zip", "S3Bucket": "foobar", }) def test_with_zip_file(self): function = SamFunction("foo") function.InlineCode = "hello world" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [ x for x in cfnResources if isinstance(x, LambdaFunction) ] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, {"ZipFile": "hello world"}) def test_with_no_code_uri_or_zipfile(self): function = SamFunction("foo") with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs)
class TestLayers(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": {"foo": "bar"}, } def test_basic_layer(self): layer = SamLayerVersion("foo") layer.ContentUri = "s3://foobar/foo.zip" cfnResources = layer.to_cloudformation(**self.kwargs) generatedLayerList = [x for x in cfnResources if isinstance(x, LambdaLayerVersion)] self.assertEqual(cfnResources.__len__(), 1) self.assertTrue(isinstance(cfnResources[0], LambdaLayerVersion)) self.assertEqual(cfnResources[0].Content, {"S3Key": "foo.zip", "S3Bucket": "foobar"}) def test_invalid_compatible_architectures(self): layer = SamLayerVersion("foo") layer.ContentUri = "s3://foobar/foo.zip" invalid_architectures = [["arm"], [1], "arm", 1, True] for architecturea in invalid_architectures: layer.CompatibleArchitectures = architecturea with pytest.raises(InvalidResourceException): layer.to_cloudformation(**self.kwargs)
def test_throw_on_empty_parameters(self): with self.assertRaises(InvalidDocumentException): IntrinsicsResolver(None).resolve_parameter_refs({})
def translate(self, sam_template, parameter_values): """Loads the SAM resources from the given SAM manifest, replaces them with their corresponding CloudFormation resources, and returns the resulting CloudFormation template. :param dict sam_template: the SAM manifest, as loaded by json.load() or yaml.load(), or as provided by \ CloudFormation transforms. :param dict parameter_values: Map of template parameter names to their values. It is a required parameter that should at least be an empty map. By providing an empty map, the caller explicitly opts-into the idea that some functionality that relies on resolving parameter references might not work as expected (ex: auto-creating new Lambda Version when CodeUri contains reference to template parameter). This is why this parameter is required :returns: a copy of the template with SAM resources replaced with the corresponding CloudFormation, which may \ be dumped into a valid CloudFormation JSON or YAML template """ sam_parameter_values = SamParameterValues(parameter_values) sam_parameter_values.add_default_parameter_values(sam_template) sam_parameter_values.add_pseudo_parameter_values() parameter_values = sam_parameter_values.parameter_values # Create & Install plugins sam_plugins = prepare_plugins(self.plugins, parameter_values) self.sam_parser.parse( sam_template=sam_template, parameter_values=parameter_values, sam_plugins=sam_plugins ) template = copy.deepcopy(sam_template) macro_resolver = ResourceTypeResolver(sam_resources) intrinsics_resolver = IntrinsicsResolver(parameter_values) deployment_preference_collection = DeploymentPreferenceCollection() supported_resource_refs = SupportedResourceReferences() document_errors = [] changed_logical_ids = {} for logical_id, resource_dict in self._get_resources_to_iterate(sam_template, macro_resolver): try: macro = macro_resolver\ .resolve_resource_type(resource_dict)\ .from_dict(logical_id, resource_dict, sam_plugins=sam_plugins) kwargs = macro.resources_to_link(sam_template['Resources']) kwargs['managed_policy_map'] = self.managed_policy_map kwargs['intrinsics_resolver'] = intrinsics_resolver kwargs['deployment_preference_collection'] = deployment_preference_collection translated = macro.to_cloudformation(**kwargs) supported_resource_refs = macro.get_resource_references(translated, supported_resource_refs) # Some resources mutate their logical ids. Track those to change all references to them: if logical_id != macro.logical_id: changed_logical_ids[logical_id] = macro.logical_id del template['Resources'][logical_id] for resource in translated: if verify_unique_logical_id(resource, sam_template['Resources']): template['Resources'].update(resource.to_dict()) else: document_errors.append(DuplicateLogicalIdException( logical_id, resource.logical_id, resource.resource_type)) except (InvalidResourceException, InvalidEventException) as e: document_errors.append(e) if deployment_preference_collection.any_enabled(): template['Resources'].update(deployment_preference_collection.codedeploy_application.to_dict()) if not deployment_preference_collection.can_skip_service_role(): template['Resources'].update(deployment_preference_collection.codedeploy_iam_role.to_dict()) for logical_id in deployment_preference_collection.enabled_logical_ids(): template['Resources'].update(deployment_preference_collection.deployment_group(logical_id).to_dict()) # Run the after-transform plugin target try: sam_plugins.act(LifeCycleEvents.after_transform_template, template) except (InvalidDocumentException, InvalidResourceException) as e: document_errors.append(e) # Cleanup if 'Transform' in template: del template['Transform'] if len(document_errors) == 0: template = intrinsics_resolver.resolve_sam_resource_id_refs(template, changed_logical_ids) template = intrinsics_resolver.resolve_sam_resource_refs(template, supported_resource_refs) return template else: raise InvalidDocumentException(document_errors)
def translate(self, sam_template, parameter_values): """Loads the SAM resources from the given SAM manifest, replaces them with their corresponding CloudFormation resources, and returns the resulting CloudFormation template. :param dict sam_template: the SAM manifest, as loaded by json.load() or yaml.load(), or as provided by \ CloudFormation transforms. :param dict parameter_values: Map of template parameter names to their values. It is a required parameter that should at least be an empty map. By providing an empty map, the caller explicitly opts-into the idea that some functionality that relies on resolving parameter references might not work as expected (ex: auto-creating new Lambda Version when CodeUri contains reference to template parameter). This is why this parameter is required :returns: a copy of the template with SAM resources replaced with the corresponding CloudFormation, which may \ be dumped into a valid CloudFormation JSON or YAML template """ # Create & Install plugins sam_plugins = prepare_plugins(self.plugins) parameter_values = self._add_default_parameter_values( sam_template, parameter_values) self.sam_parser.parse(sam_template=sam_template, parameter_values=parameter_values, sam_plugins=sam_plugins) template = copy.deepcopy(sam_template) macro_resolver = ResourceTypeResolver(sam_resources) intrinsics_resolver = IntrinsicsResolver(parameter_values) deployment_preference_collection = DeploymentPreferenceCollection() supported_resource_refs = SupportedResourceReferences() document_errors = [] changed_logical_ids = {} for logical_id, resource_dict in self._get_resources_to_iterate( sam_template, macro_resolver): try: macro = macro_resolver\ .resolve_resource_type(resource_dict)\ .from_dict(logical_id, resource_dict, sam_plugins=sam_plugins) kwargs = macro.resources_to_link(sam_template['Resources']) kwargs['managed_policy_map'] = self.managed_policy_map kwargs['intrinsics_resolver'] = intrinsics_resolver kwargs[ 'deployment_preference_collection'] = deployment_preference_collection translated = macro.to_cloudformation(**kwargs) supported_resource_refs = macro.get_resource_references( translated, supported_resource_refs) # Some resources mutate their logical ids. Track those to change all references to them: if logical_id != macro.logical_id: changed_logical_ids[logical_id] = macro.logical_id del template['Resources'][logical_id] for resource in translated: if verify_unique_logical_id(resource, sam_template['Resources']): template['Resources'].update(resource.to_dict()) else: document_errors.append( DuplicateLogicalIdException( logical_id, resource.logical_id, resource.resource_type)) except (InvalidResourceException, InvalidEventException) as e: document_errors.append(e) if deployment_preference_collection.any_enabled(): template['Resources'].update(deployment_preference_collection. codedeploy_application.to_dict()) if not deployment_preference_collection.can_skip_service_role(): template['Resources'].update(deployment_preference_collection. codedeploy_iam_role.to_dict()) for logical_id in deployment_preference_collection.enabled_logical_ids( ): template['Resources'].update( deployment_preference_collection.deployment_group( logical_id).to_dict()) # Run the after-transform plugin target try: sam_plugins.act(LifeCycleEvents.after_transform_template, template) except (InvalidDocumentException, InvalidResourceException) as e: document_errors.append(e) # Cleanup if 'Transform' in template: del template['Transform'] if len(document_errors) is 0: template = intrinsics_resolver.resolve_sam_resource_id_refs( template, changed_logical_ids) template = intrinsics_resolver.resolve_sam_resource_refs( template, supported_resource_refs) return template else: raise InvalidDocumentException(document_errors)
def _get_intrinsic_resolvers(self, mappings): return [ IntrinsicsResolver(self._parameters), IntrinsicsResolver( mappings, {FindInMapAction.intrinsic_name: FindInMapAction()}), ]
def test_throw_on_non_dict_parameters(self): with self.assertRaises(InvalidDocumentException): IntrinsicsResolver([1, 2, 3]).resolve_parameter_refs({})
class TestParameterReferenceResolution(TestCase): def setUp(self): self.parameter_values = { "param1": "value1", "param2": "value2", "param3": "value3" } self.resolver = IntrinsicsResolver(self.parameter_values) def test_must_resolve_top_level_direct_refs(self): input = { "key1": { "Ref": "param1" }, "key2": { "Ref": "param2" }, "key3": { "a": "b" } } expected = { "key1": self.parameter_values["param1"], "key2": self.parameter_values["param2"], "key3": { "a": "b" }, } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_nested_refs(self): input = { "key1": { "sub1": { "sub2": { "sub3": { "Ref": "param1" }, "list": [1, "b", { "Ref": "param2" }] } } } } expected = { "key1": { "sub1": { "sub2": { "sub3": self.parameter_values["param1"], "list": [1, "b", self.parameter_values["param2"]] } } } } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_direct_refs(self): input = {"Ref": "param1"} expected = self.parameter_values["param1"] output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_array_refs(self): input = ["foo", 1, 2, {"Ref": "param1"}] expected = ["foo", 1, 2, self.parameter_values["param1"]] output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_skip_unknown_refs(self): input = {"key1": {"Ref": "someresource"}, "key2": {"Ref": "param1"}} expected = { "key1": { "Ref": "someresource" }, "key2": self.parameter_values["param1"] } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_inside_sub_strings(self): input = { "Fn::Sub": "prefix ${param1} ${param2} ${param3} ${param1} suffix" } expected = {"Fn::Sub": "prefix value1 value2 value3 value1 suffix"} output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_skip_over_sub_literals(self): input = {"Fn::Sub": "prefix ${!MustNotBeReplaced} suffix"} expected = {"Fn::Sub": "prefix ${!MustNotBeReplaced} suffix"} output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_refs_inside_other_intrinsics(self): input = { "key1": { "Fn::Join": ["-", [{ "Ref": "param1" }, "some other value"]] } } expected = { "key1": { "Fn::Join": ["-", [self.parameter_values["param1"], "some other value"]] } } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_skip_invalid_values_for_ref(self): input = {"Ref": ["ref cannot have list value"]} expected = {"Ref": ["ref cannot have list value"]} output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_skip_invalid_values_for_sub(self): input = { # Invalid Sub resource, must never be parsed, and must not error out "Fn::Sub": [{ "a": "b" }] } expected = {"Fn::Sub": [{"a": "b"}]} output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_throw_on_empty_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver(None).resolve_parameter_refs({}) def test_throw_on_non_dict_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver([1, 2, 3]).resolve_parameter_refs({}) def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_parameter_refs = Mock( ) # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_parameter_refs(input), expected) resolver._try_resolve_parameter_refs.assert_not_called()
class TestHttpApiDescription(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": {"foo": "bar"}, } @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_no_description(self): sam_http_api = SamHttpApi("foo") sam_http_api.DefinitionBody = { "openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}}, "info": {"description": "existing description"}, } resources = sam_http_api.to_cloudformation(**self.kwargs) http_api = [x for x in resources if isinstance(x, ApiGatewayV2HttpApi)] self.assertEqual(http_api[0].Body.get("info", {}).get("description"), "existing description") @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_no_definition_body(self): sam_http_api = SamHttpApi("foo") sam_http_api.Description = "my description" with self.assertRaises(InvalidResourceException) as context: sam_http_api.to_cloudformation(**self.kwargs) self.assertEqual( context.exception.message, "Resource with id [foo] is invalid. " "Description works only with inline OpenApi specified in the 'DefinitionBody' property.", ) @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_description_defined_in_definition_body(self): sam_http_api = SamHttpApi("foo") sam_http_api.DefinitionBody = { "openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}}, "info": {"description": "existing description"}, } sam_http_api.Description = "new description" with self.assertRaises(InvalidResourceException) as context: sam_http_api.to_cloudformation(**self.kwargs) self.assertEqual( context.exception.message, "Resource with id [foo] is invalid. " "Unable to set Description because it is already defined within inline OpenAPI specified in the " "'DefinitionBody' property.", ) @patch("boto3.session.Session.region_name", "eu-central-1") def test_with_description_not_defined_in_definition_body(self): sam_http_api = SamHttpApi("foo") sam_http_api.DefinitionBody = {"openapi": "3.0.1", "paths": {"/foo": {}}, "info": {}} sam_http_api.Description = "new description" resources = sam_http_api.to_cloudformation(**self.kwargs) http_api = [x for x in resources if isinstance(x, ApiGatewayV2HttpApi)] self.assertEqual(http_api[0].Body.get("info", {}).get("description"), "new description")
def setUp(self): self.resolver = IntrinsicsResolver({})
def setUp(self): self.resolver = IntrinsicsResolver({})
class TestParameterReferenceResolution(TestCase): def setUp(self): self.parameter_values = { "param1": "value1", "param2": "value2", "param3": "value3" } self.resolver = IntrinsicsResolver(self.parameter_values) def test_must_resolve_top_level_direct_refs(self): input = { "key1": { "Ref": "param1" }, "key2": { "Ref": "param2" }, "key3": { "a": "b" } } expected = { "key1": self.parameter_values["param1"], "key2": self.parameter_values["param2"], "key3": { "a": "b" } } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_nested_refs(self): input = { "key1": { "sub1": { "sub2": { "sub3": { "Ref": "param1" }, "list": [1, "b", {"Ref": "param2"}] } } } } expected = { "key1": { "sub1": { "sub2": { "sub3": self.parameter_values["param1"], "list": [1, "b", self.parameter_values["param2"]] } } } } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_direct_refs(self): input = {"Ref": "param1"} expected = self.parameter_values["param1"] output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_array_refs(self): input = ["foo", 1, 2, {"Ref": "param1"}] expected = ["foo", 1, 2, self.parameter_values["param1"]] output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_skip_unknown_refs(self): input = { "key1": { "Ref": "someresource" }, "key2": { "Ref": "param1" } } expected = { "key1": { "Ref": "someresource" }, "key2": self.parameter_values["param1"] } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_inside_sub_strings(self): input = { "Fn::Sub": "prefix ${param1} ${param2} ${param3} ${param1} suffix" } expected = { "Fn::Sub": "prefix value1 value2 value3 value1 suffix" } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_skip_over_sub_literals(self): input = { "Fn::Sub": "prefix ${!MustNotBeReplaced} suffix" } expected = { "Fn::Sub": "prefix ${!MustNotBeReplaced} suffix" } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_must_resolve_refs_inside_other_intrinsics(self): input = { "key1": { "Fn::Join": ["-", [{"Ref": "param1"}, "some other value"]] } } expected = { "key1": { "Fn::Join": ["-", [self.parameter_values["param1"], "some other value"]] } } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_skip_invalid_values_for_ref(self): input = { "Ref": ["ref cannot have list value"] } expected = { "Ref": ["ref cannot have list value"] } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_skip_invalid_values_for_sub(self): input = { # Invalid Sub resource, must never be parsed, and must not error out "Fn::Sub": [{"a": "b"}] } expected = { "Fn::Sub": [{"a": "b"}] } output = self.resolver.resolve_parameter_refs(input) self.assertEqual(output, expected) def test_throw_on_empty_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver(None).resolve_parameter_refs({}) def test_throw_on_non_dict_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver([1,2,3]).resolve_parameter_refs({}) def test_short_circuit_on_empty_parameters(self): resolver = IntrinsicsResolver({}) resolver._try_resolve_parameter_refs = Mock() # Mock other methods to detect any actual calls input = {"Ref": "foo"} expected = {"Ref": "foo"} self.assertEqual(resolver.resolve_parameter_refs(input), expected) resolver._try_resolve_parameter_refs.assert_not_called()
def test_throw_on_non_dict_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver([1, 2, 3]).resolve_parameter_refs({})
def test_throw_on_empty_parameters(self): with self.assertRaises(TypeError): IntrinsicsResolver(None).resolve_parameter_refs({})
class TestArchitecture(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": {"foo": "bar"}, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_unknown_architectures(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" invalid_architectures = [["arm"], [1], "arm", 1, {"my": "value"}, True, [], {}] for architecture in invalid_architectures: function.Architectures = architecture with pytest.raises(InvalidResourceException) as e: function.to_cloudformation(**self.kwargs) self.assertEqual( str(e.value.message), "Resource with id [foo] is invalid. Architectures needs to be a list with one string, either `x86_64` or `arm64`.", ) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_multiple_architectures(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" function.Architectures = ["arm64", "x86_64"] with pytest.raises(InvalidResourceException) as e: function.to_cloudformation(**self.kwargs) self.assertEqual( str(e.value.message), "Resource with id [foo] is invalid. Architectures needs to be a list with one string, either `x86_64` or `arm64`.", ) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_validate_architecture_with_intrinsic(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" function.Architectures = {"Ref": "MyRef"} cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Architectures, {"Ref": "MyRef"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_valid_architectures(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" valid_architectures = (["arm64"], ["x86_64"]) for architecture in valid_architectures: function.Architectures = architecture cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [x for x in cfnResources if isinstance(x, LambdaFunction)] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Architectures, architecture)
def translate(self, sam_template, parameter_values, feature_toggle=None): """Loads the SAM resources from the given SAM manifest, replaces them with their corresponding CloudFormation resources, and returns the resulting CloudFormation template. :param dict sam_template: the SAM manifest, as loaded by json.load() or yaml.load(), or as provided by \ CloudFormation transforms. :param dict parameter_values: Map of template parameter names to their values. It is a required parameter that should at least be an empty map. By providing an empty map, the caller explicitly opts-into the idea that some functionality that relies on resolving parameter references might not work as expected (ex: auto-creating new Lambda Version when CodeUri contains reference to template parameter). This is why this parameter is required :returns: a copy of the template with SAM resources replaced with the corresponding CloudFormation, which may \ be dumped into a valid CloudFormation JSON or YAML template """ self.feature_toggle = (feature_toggle if feature_toggle else FeatureToggle( FeatureToggleDefaultConfigProvider(), stage=None, account_id=None, region=None)) self.function_names = dict() self.redeploy_restapi_parameters = dict() sam_parameter_values = SamParameterValues(parameter_values) sam_parameter_values.add_default_parameter_values(sam_template) sam_parameter_values.add_pseudo_parameter_values(self.boto_session) parameter_values = sam_parameter_values.parameter_values # Create & Install plugins sam_plugins = prepare_plugins(self.plugins, parameter_values) self.sam_parser.parse(sam_template=sam_template, parameter_values=parameter_values, sam_plugins=sam_plugins) template = copy.deepcopy(sam_template) macro_resolver = ResourceTypeResolver(sam_resources) intrinsics_resolver = IntrinsicsResolver(parameter_values) mappings_resolver = IntrinsicsResolver( template.get("Mappings", {}), {FindInMapAction.intrinsic_name: FindInMapAction()}) deployment_preference_collection = DeploymentPreferenceCollection() supported_resource_refs = SupportedResourceReferences() shared_api_usage_plan = SharedApiUsagePlan() document_errors = [] changed_logical_ids = {} for logical_id, resource_dict in self._get_resources_to_iterate( sam_template, macro_resolver): try: macro = macro_resolver.resolve_resource_type( resource_dict).from_dict(logical_id, resource_dict, sam_plugins=sam_plugins) kwargs = macro.resources_to_link(sam_template["Resources"]) kwargs["managed_policy_map"] = self.managed_policy_map kwargs["intrinsics_resolver"] = intrinsics_resolver kwargs["mappings_resolver"] = mappings_resolver kwargs[ "deployment_preference_collection"] = deployment_preference_collection kwargs["conditions"] = template.get("Conditions") # add the value of FunctionName property if the function is referenced with the api resource self.redeploy_restapi_parameters[ "function_names"] = self._get_function_names( resource_dict, intrinsics_resolver) kwargs[ "redeploy_restapi_parameters"] = self.redeploy_restapi_parameters kwargs["shared_api_usage_plan"] = shared_api_usage_plan translated = macro.to_cloudformation(**kwargs) supported_resource_refs = macro.get_resource_references( translated, supported_resource_refs) # Some resources mutate their logical ids. Track those to change all references to them: if logical_id != macro.logical_id: changed_logical_ids[logical_id] = macro.logical_id del template["Resources"][logical_id] for resource in translated: if verify_unique_logical_id(resource, sam_template["Resources"]): template["Resources"].update(resource.to_dict()) else: document_errors.append( DuplicateLogicalIdException( logical_id, resource.logical_id, resource.resource_type)) except (InvalidResourceException, InvalidEventException) as e: document_errors.append(e) if deployment_preference_collection.any_enabled(): template["Resources"].update(deployment_preference_collection. codedeploy_application.to_dict()) if not deployment_preference_collection.can_skip_service_role(): template["Resources"].update(deployment_preference_collection. codedeploy_iam_role.to_dict()) for logical_id in deployment_preference_collection.enabled_logical_ids( ): try: template["Resources"].update( deployment_preference_collection.deployment_group( logical_id).to_dict()) except InvalidResourceException as e: document_errors.append(e) # Run the after-transform plugin target try: sam_plugins.act(LifeCycleEvents.after_transform_template, template) except (InvalidDocumentException, InvalidResourceException) as e: document_errors.append(e) # Cleanup if "Transform" in template: del template["Transform"] if len(document_errors) == 0: template = intrinsics_resolver.resolve_sam_resource_id_refs( template, changed_logical_ids) template = intrinsics_resolver.resolve_sam_resource_refs( template, supported_resource_refs) return template else: raise InvalidDocumentException(document_errors)
class TestCodeUriandImageUri(TestCase): kwargs = { "intrinsics_resolver": IntrinsicsResolver({}), "event_resources": [], "managed_policy_map": { "foo": "bar" }, } @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_code_uri(self): function = SamFunction("foo") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "foo" function.Handler = "bar" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [ x for x in cfnResources if isinstance(x, LambdaFunction) ] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, { "S3Key": "foo.zip", "S3Bucket": "foobar" }) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_zip_file(self): function = SamFunction("foo") function.InlineCode = "hello world" function.Runtime = "foo" function.Handler = "bar" cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [ x for x in cfnResources if isinstance(x, LambdaFunction) ] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, {"ZipFile": "hello world"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_no_code_uri_or_zipfile_or_no_image_uri(self): function = SamFunction("foo") with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_image_uri(self): function = SamFunction("foo") function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest" function.PackageType = IMAGE cfnResources = function.to_cloudformation(**self.kwargs) generatedFunctionList = [ x for x in cfnResources if isinstance(x, LambdaFunction) ] self.assertEqual(generatedFunctionList.__len__(), 1) self.assertEqual(generatedFunctionList[0].Code, {"ImageUri": function.ImageUri}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_image_uri_layers_runtime_handler(self): function = SamFunction("foo") function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest" function.Layers = ["Layer1"] function.Runtime = "foo" function.Handler = "bar" function.PackageType = IMAGE with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_image_uri_package_type_zip(self): function = SamFunction("foo") function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest" function.PackageType = ZIP with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_image_uri_invalid_package_type(self): function = SamFunction("foo") function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest" function.PackageType = "fake" with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_with_image_uri_and_code_uri(self): function = SamFunction("foo") function.ImageUri = "123456789.dkr.ecr.us-east-1.amazonaws.com/myimage:latest" function.CodeUri = "s3://foobar/foo.zip" with pytest.raises(InvalidResourceException): function.to_cloudformation(**self.kwargs)
def test_configure_supported_intrinsics_must_error_for_non_dict_input( self): with self.assertRaises(TypeError): IntrinsicsResolver({}, [1, 2, 3])