From c12ad29bf90b7254910825feddfe8833f8a5276e Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Wed, 7 Nov 2018 18:20:32 +0100 Subject: [PATCH 01/14] Condition handling (for issue #142) --- samtranslator/model/__init__.py | 4 +- samtranslator/model/sam_resources.py | 2 +- .../input/function_with_condition.yaml | 8 +++ .../aws-cn/function_with_condition.json | 52 +++++++++++++++++++ .../aws-us-gov/function_with_condition.json | 52 +++++++++++++++++++ .../output/function_with_condition.json | 52 +++++++++++++++++++ tests/translator/test_translator.py | 1 + 7 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 tests/translator/input/function_with_condition.yaml create mode 100644 tests/translator/output/aws-cn/function_with_condition.json create mode 100644 tests/translator/output/aws-us-gov/function_with_condition.json create mode 100644 tests/translator/output/function_with_condition.json diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index ecc354d6f..b5fc77a55 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -38,7 +38,7 @@ class Resource(object): property_types = None _keywords = ['logical_id', 'relative_id', "depends_on", "resource_attributes"] - _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy"] + _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition"] # Runtime attributes that can be qureied resource. They are CloudFormation attributes like ARN, Name etc that # will be resolvable at runtime. This map will be implemented by sub-classes to express list of attributes they @@ -57,7 +57,7 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non to identify sub-resources. :param depends_on Value of DependsOn resource attribute :param attributes Dictionary of resource attributes and their values - """ + """ self._validate_logical_id(logical_id) self.logical_id = logical_id self.relative_id = relative_id diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index cc5916b5b..a57cbc733 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -154,7 +154,7 @@ def _construct_lambda_function(self): :returns: a list containing the Lambda function and execution role resources :rtype: list """ - lambda_function = LambdaFunction(self.logical_id, depends_on=self.depends_on) + lambda_function = LambdaFunction(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) if self.FunctionName: lambda_function.FunctionName = self.FunctionName diff --git a/tests/translator/input/function_with_condition.yaml b/tests/translator/input/function_with_condition.yaml new file mode 100644 index 000000000..ed32206bc --- /dev/null +++ b/tests/translator/input/function_with_condition.yaml @@ -0,0 +1,8 @@ +Resources: + ConditionFunction: + Type: 'AWS::Serverless::Function' + Condition: "this is a test" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_condition.json b/tests/translator/output/aws-cn/function_with_condition.json new file mode 100644 index 000000000..dc6e0bca2 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_condition.json b/tests/translator/output/aws-us-gov/function_with_condition.json new file mode 100644 index 000000000..c2d22acf1 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_condition.json b/tests/translator/output/function_with_condition.json new file mode 100644 index 000000000..fc7af1fde --- /dev/null +++ b/tests/translator/output/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index a67238738..9bf52ce0f 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -93,6 +93,7 @@ class TestTranslatorEndToEnd(TestCase): @parameterized.expand( itertools.product([ + 'function_with_condition', 'basic_function', 'cloudwatchevent', 'cloudwatch_logs_with_ref', From fcabba2728f0c2f63a02c8be63a7388fa7d3482e Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Wed, 7 Nov 2018 18:36:02 +0100 Subject: [PATCH 02/14] Update __init__.py Whitespce correction --- samtranslator/model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index b5fc77a55..430dadfba 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -57,7 +57,7 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non to identify sub-resources. :param depends_on Value of DependsOn resource attribute :param attributes Dictionary of resource attributes and their values - """ + """ self._validate_logical_id(logical_id) self.logical_id = logical_id self.relative_id = relative_id From abd805fad997172bd6c12b4361da0cd4fc5c795b Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sat, 10 Nov 2018 18:56:59 +0100 Subject: [PATCH 03/14] General - Added get_passthrough_resource_attributes to Resource currently only returns Condition - Added make_conditional to intrinsics surrounds data with Fn:If and returns AWS::NoValue if the condition is false SamFunction - _construct_role of SamFunction now passes condition to created role - _construct_version of SamFunction now adds condition to version - _construct_alias of SamFunction now passes Condition to alias PushEvents - All events: _construct_permission passes passthrough attributes - CloudWatchEvent gives event_rule same condition as function - S3 gives event_rule same condition as function - S3 Conditional DependsOn for the bucket (undocumented used!!) (see comments) - S3 conditional LambdaConfigurations added Tests - Added test case s3_with_condition (+updated function_with_condition) - test_cloudwatchlogs_event_source.py mock fixed Still not done... This is harder than I though, some help would be appreciated :-) --- samtranslator/model/__init__.py | 13 ++- samtranslator/model/eventsources/push.py | 45 +++++-- samtranslator/model/intrinsics.py | 8 ++ samtranslator/model/sam_resources.py | 13 ++- .../test_cloudwatchlogs_event_source.py | 1 + tests/translator/input/s3_with_condition.yaml | 18 +++ .../aws-cn/function_with_condition.json | 1 + .../output/aws-cn/s3_with_condition.json | 110 ++++++++++++++++++ .../aws-us-gov/function_with_condition.json | 1 + .../output/aws-us-gov/s3_with_condition.json | 110 ++++++++++++++++++ .../output/function_with_condition.json | 1 + .../translator/output/s3_with_condition.json | 110 ++++++++++++++++++ tests/translator/test_translator.py | 1 + 13 files changed, 418 insertions(+), 14 deletions(-) create mode 100644 tests/translator/input/s3_with_condition.yaml create mode 100644 tests/translator/output/aws-cn/s3_with_condition.json create mode 100644 tests/translator/output/aws-us-gov/s3_with_condition.json create mode 100644 tests/translator/output/s3_with_condition.json diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index 430dadfba..d43b5aee8 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -302,7 +302,18 @@ def get_runtime_attr(self, attr_name): else: raise NotImplementedError(attr_name + " attribute is not implemented for resource " + self.resource_type) - + def get_passthrough_resource_attributes(self): + """ + Returns a dictionary of resource attributes of the ResourceMacro that should be passed through from the main + vanilla CloudFormation resource to its children. Currently only Condition is copied. + + :return: Dictionary of resource attributes. + """ + attributes = None + if 'Condition' in self.resource_attributes: + attributes = { 'Condition': self.resource_attributes['Condition'] } + return attributes + class ResourceMacro(Resource): """A ResourceMacro object represents a CloudFormation macro. A macro appears in the CloudFormation template in the "Resources" mapping, but must be expanded into one or more vanilla CloudFormation resources before a stack can be diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 007096d3b..98c0dfc88 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -2,7 +2,8 @@ from six import string_types from samtranslator.model import ResourceMacro, PropertyType from samtranslator.model.types import is_type, list_of, dict_of, one_of, is_str -from samtranslator.model.intrinsics import ref, fnSub, make_shorthand +from samtranslator.model.intrinsics import ref, fnSub, fnGetAtt, make_shorthand, make_conditional +from samtranslator.model.tags.resource_tagging import get_tag_list from samtranslator.model.s3 import S3Bucket from samtranslator.model.sns import SNSSubscription @@ -43,7 +44,7 @@ def _construct_permission(self, function, source_arn=None, source_account=None, :returns: the permission resource :rtype: model.lambda_.LambdaPermission """ - lambda_permission = LambdaPermission(self.logical_id + 'Permission' + suffix) + lambda_permission = LambdaPermission(self.logical_id + 'Permission' + suffix, attributes=function.get_passthrough_resource_attributes()) try: # Name will not be available for Alias resources @@ -138,12 +139,13 @@ def to_cloudformation(self, **kwargs): resources = [] events_rule = EventsRule(self.logical_id) - resources.append(events_rule) - events_rule.EventPattern = self.Pattern events_rule.Targets = [self._construct_target(function)] - source_arn = events_rule.get_runtime_attr("arn") + if 'Condition' in function.resource_attributes: + events_rule = make_conditional(events_rule, function.resource_attributes['Condition']) + resources.append(events_rule) + source_arn = events_rule.get_runtime_attr("arn") resources.append(self._construct_permission(function, source_arn=source_arn)) return resources @@ -210,7 +212,10 @@ def to_cloudformation(self, **kwargs): source_account = ref('AWS::AccountId') permission = self._construct_permission(function, source_account=source_account) - self._depend_on_lambda_permissions(bucket, permission) + if 'Condition' in permission.resource_attributes: + self._depend_on_lambda_permissions_using_tag(bucket, permission) + else: + self._depend_on_lambda_permissions(bucket, permission) resources.append(permission) # NOTE: `bucket` here is a dictionary representing the S3 Bucket resource in your SAM template. If there are @@ -253,6 +258,31 @@ def _depend_on_lambda_permissions(self, bucket, permission): return bucket + def _depend_on_lambda_permissions_using_tag(self, bucket, permission): + """ + Since conditional DependsOn is not supported this undocumented way of + implicitely making dependency through tags is used. + """ + properties = bucket.get('Properties', None) + if properties is None: + properties = {} + bucket['Properties'] = properties + tags = properties.get('Tags', None) + if tags is None: + tags = [] + properties['Tags'] = tags + dep_tag = { + 'sam:ConditionalDependsOn:' + permission.logical_id: { + 'Fn:If': [ + permission.resource_attributes['Condition'], + fnGetAtt(permission.logical_id, 'Arn'), + 'no dpendency' + ] + } + } + properties['Tags'] = tags + get_tag_list(dep_tag) + return bucket + def _inject_notification_configuration(self, function, bucket): base_event_mapping = { 'Function': function.get_runtime_attr("arn") @@ -270,7 +300,8 @@ def _inject_notification_configuration(self, function, bucket): lambda_event = copy.deepcopy(base_event_mapping) lambda_event['Event'] = event_type - + if 'Condition' in function.resource_attributes: + lambda_event = make_conditional(lambda_event, function.resource_attributes['Condition']) event_mappings.append(lambda_event) properties = bucket.get('Properties', None) diff --git a/samtranslator/model/intrinsics.py b/samtranslator/model/intrinsics.py index fd5e8e0e7..64e43d973 100644 --- a/samtranslator/model/intrinsics.py +++ b/samtranslator/model/intrinsics.py @@ -15,6 +15,14 @@ def fnSub(string, variables=None): return {'Fn::Sub': [string, variables]} return {'Fn::Sub': string} +def make_conditional(condition, data): + return { + 'Fn::If': [ + condition, + data, + { 'Ref': 'AWS::NoValue' } + ] + } def make_shorthand(intrinsic_dict): """ diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index a57cbc733..e099bab12 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -202,7 +202,7 @@ def _construct_role(self, managed_policy_map): :returns: the generated IAM Role :rtype: model.iam.IAMRole """ - execution_role = IAMRole(self.logical_id + 'Role') + execution_role = IAMRole(self.logical_id + 'Role', attributes=self.get_passthrough_resource_attributes()) execution_role.AssumeRolePolicyDocument = IAMRolePolicies.lambda_assume_role_policy() managed_policy_arns = [ArnGenerator.generate_aws_managed_policy_arn('service-role/AWSLambdaBasicExecutionRole')] @@ -399,11 +399,12 @@ def _construct_version(self, function, intrinsics_resolver): prefix = "{id}Version".format(id=self.logical_id) logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict).gen() - retain_old_versions = { - "DeletionPolicy": "Retain" - } + attributes = self.get_passthrough_resource_attributes() + if attributes is None: + attributes = {} + attributes["DeletionPolicy"] = "Retain" - lambda_version = LambdaVersion(logical_id=logical_id, attributes=retain_old_versions) + lambda_version = LambdaVersion(logical_id=logical_id, attributes=attributes) lambda_version.FunctionName = function.get_runtime_attr('name') return lambda_version @@ -422,7 +423,7 @@ def _construct_alias(self, name, function, version): raise ValueError("Alias name is required to create an alias") logical_id = "{id}Alias{suffix}".format(id=function.logical_id, suffix=name) - alias = LambdaAlias(logical_id=logical_id) + alias = LambdaAlias(logical_id=logical_id, attributes=self.get_passthrough_resource_attributes()) alias.Name = name alias.FunctionName = function.get_runtime_attr('name') alias.FunctionVersion = version.get_runtime_attr("version") diff --git a/tests/model/eventsources/test_cloudwatchlogs_event_source.py b/tests/model/eventsources/test_cloudwatchlogs_event_source.py index 22d760a1a..f49cb7975 100644 --- a/tests/model/eventsources/test_cloudwatchlogs_event_source.py +++ b/tests/model/eventsources/test_cloudwatchlogs_event_source.py @@ -14,6 +14,7 @@ def setUp(self): self.function = Mock() self.function.get_runtime_attr = Mock() self.function.get_runtime_attr.return_value = 'arn:aws:mock' + self.function.get_passthrough_resource_attributes.return_value = None self.permission = Mock() self.permission.logical_id = 'LogProcessorPermission' diff --git a/tests/translator/input/s3_with_condition.yaml b/tests/translator/input/s3_with_condition.yaml new file mode 100644 index 000000000..e0f4e7988 --- /dev/null +++ b/tests/translator/input/s3_with_condition.yaml @@ -0,0 +1,18 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Condition: MyCondition + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket diff --git a/tests/translator/output/aws-cn/function_with_condition.json b/tests/translator/output/aws-cn/function_with_condition.json index dc6e0bca2..543e1445c 100644 --- a/tests/translator/output/aws-cn/function_with_condition.json +++ b/tests/translator/output/aws-cn/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/aws-cn/s3_with_condition.json b/tests/translator/output/aws-cn/s3_with_condition.json new file mode 100644 index 000000000..b0495b3c9 --- /dev/null +++ b/tests/translator/output/aws-cn/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_condition.json b/tests/translator/output/aws-us-gov/function_with_condition.json index c2d22acf1..7f7c2858d 100644 --- a/tests/translator/output/aws-us-gov/function_with_condition.json +++ b/tests/translator/output/aws-us-gov/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/aws-us-gov/s3_with_condition.json b/tests/translator/output/aws-us-gov/s3_with_condition.json new file mode 100644 index 000000000..d16c47f68 --- /dev/null +++ b/tests/translator/output/aws-us-gov/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_condition.json b/tests/translator/output/function_with_condition.json index fc7af1fde..e20488c63 100644 --- a/tests/translator/output/function_with_condition.json +++ b/tests/translator/output/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/s3_with_condition.json b/tests/translator/output/s3_with_condition.json new file mode 100644 index 000000000..e429cab24 --- /dev/null +++ b/tests/translator/output/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 9bf52ce0f..c5d9bfad5 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -93,6 +93,7 @@ class TestTranslatorEndToEnd(TestCase): @parameterized.expand( itertools.product([ + 's3_with_condition', 'function_with_condition', 'basic_function', 'cloudwatchevent', From aea43327019412d63275681245e25a756d39c1f7 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sun, 11 Nov 2018 12:25:25 +0100 Subject: [PATCH 04/14] DynamoDB table support for conditionals Push event conditionals --- samtranslator/model/eventsources/push.py | 4 ++++ samtranslator/model/sam_resources.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 98c0dfc88..11341726f 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -353,6 +353,8 @@ def _inject_subscription(self, function, topic): subscription.Protocol = 'lambda' subscription.Endpoint = function.get_runtime_attr("arn") subscription.TopicArn = topic + if 'Condition' in function.resource_attributes: + subscription = make_conditional(subscription, function.resource_attributes['Condition']) return subscription @@ -606,5 +608,7 @@ def _construct_iot_rule(self, function): payload['AwsIotSqlVersion'] = self.AwsIotSqlVersion rule.TopicRulePayload = payload + if 'Condition' in function.resource_attributes: + rule = make_conditional(rule, function.resource_attributes['Condition']) return rule diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index e099bab12..6acfb238f 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -547,7 +547,7 @@ def to_cloudformation(self, **kwargs): return [dynamodb_resources] def _construct_dynamodb_table(self): - dynamodb_table = DynamoDBTable(self.logical_id, depends_on=self.depends_on) + dynamodb_table = DynamoDBTable(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) if self.PrimaryKey: primary_key = { From 8de5e861a13cc257ab2dd0bc8053e40d6b3883d7 Mon Sep 17 00:00:00 2001 From: Vincent Botteman Date: Tue, 13 Nov 2018 19:51:43 +0100 Subject: [PATCH 05/14] Feature/sns filter policy (#648) * document new property FilterPolicy in the SNS event source * add unit test for sns event source * implement new property FilterPolicy for SNS event source * reorder the filter elements in the translated CloudFormation templates to align with the order in the original SAM template. * add example lambda_sns_filter_policy * add newline at the end of the yaml file --- .../lambda_sns_filter_policy/README.md | 35 +++++++ .../lambda_sns_filter_policy/src/index.js | 8 ++ .../lambda_sns_filter_policy/template.yaml | 24 +++++ samtranslator/model/eventsources/push.py | 10 +- samtranslator/model/sns.py | 5 +- .../validator/sam_schema/schema.json | 3 + .../eventsources/test_sns_event_source.py | 50 ++++++++++ ..._with_sns_event_source_all_parameters.yaml | 28 ++++++ ..._with_sns_event_source_all_parameters.json | 98 +++++++++++++++++++ ..._with_sns_event_source_all_parameters.json | 98 +++++++++++++++++++ ..._with_sns_event_source_all_parameters.json | 98 +++++++++++++++++++ tests/translator/test_translator.py | 1 + tests/translator/validator/test_validator.py | 1 + versions/2016-10-31.md | 8 ++ 14 files changed, 462 insertions(+), 5 deletions(-) create mode 100644 examples/2016-10-31/lambda_sns_filter_policy/README.md create mode 100644 examples/2016-10-31/lambda_sns_filter_policy/src/index.js create mode 100644 examples/2016-10-31/lambda_sns_filter_policy/template.yaml create mode 100644 tests/model/eventsources/test_sns_event_source.py create mode 100644 tests/translator/input/function_with_sns_event_source_all_parameters.yaml create mode 100644 tests/translator/output/aws-cn/function_with_sns_event_source_all_parameters.json create mode 100644 tests/translator/output/aws-us-gov/function_with_sns_event_source_all_parameters.json create mode 100644 tests/translator/output/function_with_sns_event_source_all_parameters.json diff --git a/examples/2016-10-31/lambda_sns_filter_policy/README.md b/examples/2016-10-31/lambda_sns_filter_policy/README.md new file mode 100644 index 000000000..691a3dc6e --- /dev/null +++ b/examples/2016-10-31/lambda_sns_filter_policy/README.md @@ -0,0 +1,35 @@ +# Lambda function + Filtered SNS Subscription Example + +This example shows you how to create a Lambda function with a SNS event source. + +The Lambda function does not receive all messages published to the SNS topic but only a subset. The messages are filtered based on the attributes attached to + the message. + +## Running the example + +Deploy the example into your account: + +```bash +# Replace YOUR_S3_ARTIFACTS_BUCKET with the name of a bucket which already exists in your account +aws cloudformation package --template-file template.yaml --output-template-file template.packaged.yaml --s3-bucket YOUR_S3_ARTIFACTS_BUCKET + +aws cloudformation deploy --template-file ./template.packaged.yaml --stack-name sam-example-lambda-sns-filter-policy --capabilities CAPABILITY_IAM +``` + +The Lambda function will only receive messages with the attribute `sport` set to `football`. + +In the AWS console go to the topic sam-example-lambda-sns-filter-policy and push the Publish to Topic button. +At the bottom of the Publish page you can add message attributes. Add one attribute: +- key: sport +- Attribute type: String +- value: football + +Enter an arbitrary message body and publish the message. +In Cloudwatch the log group /aws/lambda/sam-example-lambda-sns-filter-policy-notification-logger appears and the logging contains the message attributes of +the received message. + +Now publish a couple of other messages with other values for the attribute `sport` or without the attribute `sport`. +The Lambda function will not receive these messages. + +## Additional resources +https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html \ No newline at end of file diff --git a/examples/2016-10-31/lambda_sns_filter_policy/src/index.js b/examples/2016-10-31/lambda_sns_filter_policy/src/index.js new file mode 100644 index 000000000..07b954e98 --- /dev/null +++ b/examples/2016-10-31/lambda_sns_filter_policy/src/index.js @@ -0,0 +1,8 @@ +'use strict'; + + +exports.handler = async (event, context, callback) => { + console.log("Message attributes: " + JSON.stringify(event.Records[0].Sns.MessageAttributes)); + + callback(null, "Success"); +}; \ No newline at end of file diff --git a/examples/2016-10-31/lambda_sns_filter_policy/template.yaml b/examples/2016-10-31/lambda_sns_filter_policy/template.yaml new file mode 100644 index 000000000..f8dcf01d0 --- /dev/null +++ b/examples/2016-10-31/lambda_sns_filter_policy/template.yaml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Lambda function with SNS filter policy +Resources: + NotificationLogger: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./src + Handler: index.handler + Runtime: nodejs8.10 + FunctionName: sam-example-lambda-sns-filter-policy-notification-logger + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: !Ref Notifications + FilterPolicy: + sport: + - football + + Notifications: + Type: AWS::SNS::Topic + Properties: + TopicName: sam-example-lambda-sns-filter-policy diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 007096d3b..accdeb835 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -299,7 +299,8 @@ class SNS(PushEventSource): resource_type = 'SNS' principal = 'sns.amazonaws.com' property_types = { - 'Topic': PropertyType(True, is_str()) + 'Topic': PropertyType(True, is_str()), + 'FilterPolicy': PropertyType(False, dict_of(is_str(), list_of(one_of(is_str(), is_type(dict))))) } def to_cloudformation(self, **kwargs): @@ -315,14 +316,17 @@ def to_cloudformation(self, **kwargs): raise TypeError("Missing required keyword argument: function") return [self._construct_permission(function, source_arn=self.Topic), - self._inject_subscription(function, self.Topic)] + self._inject_subscription(function, self.Topic, self.FilterPolicy)] - def _inject_subscription(self, function, topic): + def _inject_subscription(self, function, topic, filterPolicy): subscription = SNSSubscription(self.logical_id) subscription.Protocol = 'lambda' subscription.Endpoint = function.get_runtime_attr("arn") subscription.TopicArn = topic + if filterPolicy is not None: + subscription.FilterPolicy = filterPolicy + return subscription diff --git a/samtranslator/model/sns.py b/samtranslator/model/sns.py index dcac1e099..7bfb1b94a 100644 --- a/samtranslator/model/sns.py +++ b/samtranslator/model/sns.py @@ -1,5 +1,5 @@ from samtranslator.model import PropertyType, Resource -from samtranslator.model.types import is_str +from samtranslator.model.types import is_type, is_str class SNSSubscription(Resource): @@ -7,5 +7,6 @@ class SNSSubscription(Resource): property_types = { 'Endpoint': PropertyType(True, is_str()), 'Protocol': PropertyType(True, is_str()), - 'TopicArn': PropertyType(True, is_str()) + 'TopicArn': PropertyType(True, is_str()), + 'FilterPolicy': PropertyType(False, is_type(dict)) } diff --git a/samtranslator/validator/sam_schema/schema.json b/samtranslator/validator/sam_schema/schema.json index 3109a9269..7f771ebc6 100644 --- a/samtranslator/validator/sam_schema/schema.json +++ b/samtranslator/validator/sam_schema/schema.json @@ -619,6 +619,9 @@ "properties": { "Topic": { "type": "string" + }, + "FilterPolicy": { + "type": "object" } }, "required": [ diff --git a/tests/model/eventsources/test_sns_event_source.py b/tests/model/eventsources/test_sns_event_source.py new file mode 100644 index 000000000..abe7eda4d --- /dev/null +++ b/tests/model/eventsources/test_sns_event_source.py @@ -0,0 +1,50 @@ +from mock import Mock +from unittest import TestCase +from samtranslator.model.eventsources.push import SNS + + +class SnsEventSource(TestCase): + + def setUp(self): + self.logical_id = 'NotificationsProcessor' + + self.sns_event_source = SNS(self.logical_id) + self.sns_event_source.Topic = 'arn:aws:sns:MyTopic' + + self.function = Mock() + self.function.get_runtime_attr = Mock() + self.function.get_runtime_attr.return_value = 'arn:aws:lambda:mock' + + def test_to_cloudformation_returns_permission_and_subscription_resources(self): + resources = self.sns_event_source.to_cloudformation( + function=self.function) + self.assertEquals(len(resources), 2) + self.assertEquals(resources[0].resource_type, + 'AWS::Lambda::Permission') + self.assertEquals(resources[1].resource_type, + 'AWS::SNS::Subscription') + + subscription = resources[1] + self.assertEquals(subscription.TopicArn, 'arn:aws:sns:MyTopic') + self.assertEquals(subscription.Protocol, 'lambda') + self.assertEquals(subscription.Endpoint, 'arn:aws:lambda:mock') + self.assertIsNone(subscription.FilterPolicy) + + def test_to_cloudformation_passes_the_filter_policy(self): + filterPolicy = { + 'attribute1': ['value1'], + 'attribute2': ['value2', 'value3'], + 'attribute3': {'numeric': ['>=', '100']} + } + self.sns_event_source.FilterPolicy = filterPolicy + + resources = self.sns_event_source.to_cloudformation( + function=self.function) + self.assertEquals(len(resources), 2) + self.assertEquals(resources[1].resource_type, + 'AWS::SNS::Subscription') + subscription = resources[1] + self.assertEquals(subscription.FilterPolicy, filterPolicy) + + def test_to_cloudformation_throws_when_no_function(self): + self.assertRaises(TypeError, self.sns_event_source.to_cloudformation) diff --git a/tests/translator/input/function_with_sns_event_source_all_parameters.yaml b/tests/translator/input/function_with_sns_event_source_all_parameters.yaml new file mode 100644 index 000000000..a745b2f9c --- /dev/null +++ b/tests/translator/input/function_with_sns_event_source_all_parameters.yaml @@ -0,0 +1,28 @@ +Resources: + MyAwesomeFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: topicArn + FilterPolicy: + store: + - example_corp + event: + - anything-but: order_cancelled + customer_interests: + - rugby + - football + - baseball + price_usd: + - numeric: + - ">=" + - 100 + + diff --git a/tests/translator/output/aws-cn/function_with_sns_event_source_all_parameters.json b/tests/translator/output/aws-cn/function_with_sns_event_source_all_parameters.json new file mode 100644 index 000000000..2a2072b9b --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_sns_event_source_all_parameters.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyAwesomeFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "store": [ + "example_corp" + ], + "event": [ + { + "anything-but": "order_cancelled" + } + ], + "customer_interests": [ + "rugby", + "football", + "baseball" + ], + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + "Endpoint": { + "Fn::GetAtt": [ + "MyAwesomeFunction", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": "topicArn" + } + }, + "MyAwesomeFunctionNotificationTopicPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "sns.amazonaws.com", + "FunctionName": { + "Ref": "MyAwesomeFunction" + }, + "SourceArn": "topicArn" + } + }, + "MyAwesomeFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MyAwesomeFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyAwesomeFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/function_with_sns_event_source_all_parameters.json b/tests/translator/output/aws-us-gov/function_with_sns_event_source_all_parameters.json new file mode 100644 index 000000000..4e682d5be --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_sns_event_source_all_parameters.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyAwesomeFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "store": [ + "example_corp" + ], + "event": [ + { + "anything-but": "order_cancelled" + } + ], + "customer_interests": [ + "rugby", + "football", + "baseball" + ], + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + "Endpoint": { + "Fn::GetAtt": [ + "MyAwesomeFunction", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": "topicArn" + } + }, + "MyAwesomeFunctionNotificationTopicPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "sns.amazonaws.com", + "FunctionName": { + "Ref": "MyAwesomeFunction" + }, + "SourceArn": "topicArn" + } + }, + "MyAwesomeFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MyAwesomeFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyAwesomeFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} diff --git a/tests/translator/output/function_with_sns_event_source_all_parameters.json b/tests/translator/output/function_with_sns_event_source_all_parameters.json new file mode 100644 index 000000000..fb7465588 --- /dev/null +++ b/tests/translator/output/function_with_sns_event_source_all_parameters.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyAwesomeFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "store": [ + "example_corp" + ], + "event": [ + { + "anything-but": "order_cancelled" + } + ], + "customer_interests": [ + "rugby", + "football", + "baseball" + ], + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + "Endpoint": { + "Fn::GetAtt": [ + "MyAwesomeFunction", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": "topicArn" + } + }, + "MyAwesomeFunctionNotificationTopicPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "sns.amazonaws.com", + "FunctionName": { + "Ref": "MyAwesomeFunction" + }, + "SourceArn": "topicArn" + } + }, + "MyAwesomeFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "MyAwesomeFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyAwesomeFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index a67238738..9fc75961f 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -151,6 +151,7 @@ class TestTranslatorEndToEnd(TestCase): 'function_with_deployment_and_custom_role', 'function_with_deployment_no_service_role', 'function_with_policy_templates', + 'function_with_sns_event_source_all_parameters', 'globals_for_function', 'globals_for_api', 'globals_for_simpletable', diff --git a/tests/translator/validator/test_validator.py b/tests/translator/validator/test_validator.py index 24462eee7..34f0fdac4 100644 --- a/tests/translator/validator/test_validator.py +++ b/tests/translator/validator/test_validator.py @@ -58,6 +58,7 @@ 'function_with_deployment_and_custom_role', 'function_with_deployment_no_service_role', 'function_with_policy_templates', + 'function_with_sns_event_source_all_parameters', 'globals_for_function', 'globals_for_api', 'globals_for_simpletable', diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md index 75d58bbd2..7b64bad6f 100644 --- a/versions/2016-10-31.md +++ b/versions/2016-10-31.md @@ -322,6 +322,7 @@ The object describing an event source with type `SNS`. Property Name | Type | Description ---|:---:|--- Topic | `string` | **Required.** Topic ARN. +FilterPolicy | [Amazon SNS filter policy](https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html) | Policy assigned to the topic subscription in order to receive only a subset of the messages. ##### Example: SNS event source object @@ -329,6 +330,13 @@ Topic | `string` | **Required.** Topic ARN. Type: SNS Properties: Topic: arn:aws:sns:us-east-1:123456789012:my_topic + FilterPolicy: + store: + - example_corp + price_usd: + - numeric: + - ">=" + - 100 ``` #### Kinesis From 6063b0e4cb6ac5c3b6010accc3eaa7173f5040ad Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sat, 24 Nov 2018 14:40:50 +0100 Subject: [PATCH 06/14] Fixes for review --- samtranslator/model/eventsources/push.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 11341726f..7d8bce641 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -262,6 +262,12 @@ def _depend_on_lambda_permissions_using_tag(self, bucket, permission): """ Since conditional DependsOn is not supported this undocumented way of implicitely making dependency through tags is used. + + See https://stackoverflow.com/questions/34607476/cloudformation-apply-condition-on-dependson + + It is done by using Fn:GetAtt wrapped in a conditional Fn:If. Using Fn:GetAtt implies a + dependency, so CloudFormation will automatically wait once it reaches that function, the same + as if you were using a DependsOn. """ properties = bucket.get('Properties', None) if properties is None: @@ -276,7 +282,7 @@ def _depend_on_lambda_permissions_using_tag(self, bucket, permission): 'Fn:If': [ permission.resource_attributes['Condition'], fnGetAtt(permission.logical_id, 'Arn'), - 'no dpendency' + 'no dependency' ] } } From 90cbfc02ccbbddc8657ceefec2405ba545e5adb5 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sun, 25 Nov 2018 07:39:50 +0100 Subject: [PATCH 07/14] Type in test outputs --- tests/translator/output/aws-cn/s3_with_condition.json | 2 +- tests/translator/output/aws-us-gov/s3_with_condition.json | 2 +- tests/translator/output/s3_with_condition.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/translator/output/aws-cn/s3_with_condition.json b/tests/translator/output/aws-cn/s3_with_condition.json index b0495b3c9..1e63b9d64 100644 --- a/tests/translator/output/aws-cn/s3_with_condition.json +++ b/tests/translator/output/aws-cn/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" diff --git a/tests/translator/output/aws-us-gov/s3_with_condition.json b/tests/translator/output/aws-us-gov/s3_with_condition.json index d16c47f68..4d5df8b17 100644 --- a/tests/translator/output/aws-us-gov/s3_with_condition.json +++ b/tests/translator/output/aws-us-gov/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" diff --git a/tests/translator/output/s3_with_condition.json b/tests/translator/output/s3_with_condition.json index e429cab24..7a4e8a393 100644 --- a/tests/translator/output/s3_with_condition.json +++ b/tests/translator/output/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" From 02460f0931eb9c4a156d877187ce84b2d6261253 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Wed, 7 Nov 2018 18:20:32 +0100 Subject: [PATCH 08/14] Condition handling (for issue #142) --- samtranslator/model/__init__.py | 4 +- samtranslator/model/sam_resources.py | 2 +- .../input/function_with_condition.yaml | 8 +++ .../aws-cn/function_with_condition.json | 52 +++++++++++++++++++ .../aws-us-gov/function_with_condition.json | 52 +++++++++++++++++++ .../output/function_with_condition.json | 52 +++++++++++++++++++ tests/translator/test_translator.py | 1 + 7 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 tests/translator/input/function_with_condition.yaml create mode 100644 tests/translator/output/aws-cn/function_with_condition.json create mode 100644 tests/translator/output/aws-us-gov/function_with_condition.json create mode 100644 tests/translator/output/function_with_condition.json diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index ecc354d6f..b5fc77a55 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -38,7 +38,7 @@ class Resource(object): property_types = None _keywords = ['logical_id', 'relative_id', "depends_on", "resource_attributes"] - _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy"] + _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition"] # Runtime attributes that can be qureied resource. They are CloudFormation attributes like ARN, Name etc that # will be resolvable at runtime. This map will be implemented by sub-classes to express list of attributes they @@ -57,7 +57,7 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non to identify sub-resources. :param depends_on Value of DependsOn resource attribute :param attributes Dictionary of resource attributes and their values - """ + """ self._validate_logical_id(logical_id) self.logical_id = logical_id self.relative_id = relative_id diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index cc5916b5b..a57cbc733 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -154,7 +154,7 @@ def _construct_lambda_function(self): :returns: a list containing the Lambda function and execution role resources :rtype: list """ - lambda_function = LambdaFunction(self.logical_id, depends_on=self.depends_on) + lambda_function = LambdaFunction(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) if self.FunctionName: lambda_function.FunctionName = self.FunctionName diff --git a/tests/translator/input/function_with_condition.yaml b/tests/translator/input/function_with_condition.yaml new file mode 100644 index 000000000..ed32206bc --- /dev/null +++ b/tests/translator/input/function_with_condition.yaml @@ -0,0 +1,8 @@ +Resources: + ConditionFunction: + Type: 'AWS::Serverless::Function' + Condition: "this is a test" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_condition.json b/tests/translator/output/aws-cn/function_with_condition.json new file mode 100644 index 000000000..dc6e0bca2 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_condition.json b/tests/translator/output/aws-us-gov/function_with_condition.json new file mode 100644 index 000000000..c2d22acf1 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_condition.json b/tests/translator/output/function_with_condition.json new file mode 100644 index 000000000..fc7af1fde --- /dev/null +++ b/tests/translator/output/function_with_condition.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "ConditionFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "this is a test", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "ConditionFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ConditionFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 9fc75961f..39d19c69b 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -93,6 +93,7 @@ class TestTranslatorEndToEnd(TestCase): @parameterized.expand( itertools.product([ + 'function_with_condition', 'basic_function', 'cloudwatchevent', 'cloudwatch_logs_with_ref', From 3627324951404c95943421a139c87f6ee5f27221 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Wed, 7 Nov 2018 18:36:02 +0100 Subject: [PATCH 09/14] Update __init__.py Whitespce correction --- samtranslator/model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index b5fc77a55..430dadfba 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -57,7 +57,7 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non to identify sub-resources. :param depends_on Value of DependsOn resource attribute :param attributes Dictionary of resource attributes and their values - """ + """ self._validate_logical_id(logical_id) self.logical_id = logical_id self.relative_id = relative_id From f1f4c01f55b6bd1b3c1a8c47175d7341f9c4e361 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sat, 10 Nov 2018 18:56:59 +0100 Subject: [PATCH 10/14] General - Added get_passthrough_resource_attributes to Resource currently only returns Condition - Added make_conditional to intrinsics surrounds data with Fn:If and returns AWS::NoValue if the condition is false SamFunction - _construct_role of SamFunction now passes condition to created role - _construct_version of SamFunction now adds condition to version - _construct_alias of SamFunction now passes Condition to alias PushEvents - All events: _construct_permission passes passthrough attributes - CloudWatchEvent gives event_rule same condition as function - S3 gives event_rule same condition as function - S3 Conditional DependsOn for the bucket (undocumented used!!) (see comments) - S3 conditional LambdaConfigurations added Tests - Added test case s3_with_condition (+updated function_with_condition) - test_cloudwatchlogs_event_source.py mock fixed Still not done... This is harder than I though, some help would be appreciated :-) --- samtranslator/model/__init__.py | 13 ++- samtranslator/model/eventsources/push.py | 45 +++++-- samtranslator/model/intrinsics.py | 8 ++ samtranslator/model/sam_resources.py | 13 ++- .../test_cloudwatchlogs_event_source.py | 1 + tests/translator/input/s3_with_condition.yaml | 18 +++ .../aws-cn/function_with_condition.json | 1 + .../output/aws-cn/s3_with_condition.json | 110 ++++++++++++++++++ .../aws-us-gov/function_with_condition.json | 1 + .../output/aws-us-gov/s3_with_condition.json | 110 ++++++++++++++++++ .../output/function_with_condition.json | 1 + .../translator/output/s3_with_condition.json | 110 ++++++++++++++++++ tests/translator/test_translator.py | 1 + 13 files changed, 418 insertions(+), 14 deletions(-) create mode 100644 tests/translator/input/s3_with_condition.yaml create mode 100644 tests/translator/output/aws-cn/s3_with_condition.json create mode 100644 tests/translator/output/aws-us-gov/s3_with_condition.json create mode 100644 tests/translator/output/s3_with_condition.json diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index 430dadfba..d43b5aee8 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -302,7 +302,18 @@ def get_runtime_attr(self, attr_name): else: raise NotImplementedError(attr_name + " attribute is not implemented for resource " + self.resource_type) - + def get_passthrough_resource_attributes(self): + """ + Returns a dictionary of resource attributes of the ResourceMacro that should be passed through from the main + vanilla CloudFormation resource to its children. Currently only Condition is copied. + + :return: Dictionary of resource attributes. + """ + attributes = None + if 'Condition' in self.resource_attributes: + attributes = { 'Condition': self.resource_attributes['Condition'] } + return attributes + class ResourceMacro(Resource): """A ResourceMacro object represents a CloudFormation macro. A macro appears in the CloudFormation template in the "Resources" mapping, but must be expanded into one or more vanilla CloudFormation resources before a stack can be diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index accdeb835..02d0ef23f 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -2,7 +2,8 @@ from six import string_types from samtranslator.model import ResourceMacro, PropertyType from samtranslator.model.types import is_type, list_of, dict_of, one_of, is_str -from samtranslator.model.intrinsics import ref, fnSub, make_shorthand +from samtranslator.model.intrinsics import ref, fnSub, fnGetAtt, make_shorthand, make_conditional +from samtranslator.model.tags.resource_tagging import get_tag_list from samtranslator.model.s3 import S3Bucket from samtranslator.model.sns import SNSSubscription @@ -43,7 +44,7 @@ def _construct_permission(self, function, source_arn=None, source_account=None, :returns: the permission resource :rtype: model.lambda_.LambdaPermission """ - lambda_permission = LambdaPermission(self.logical_id + 'Permission' + suffix) + lambda_permission = LambdaPermission(self.logical_id + 'Permission' + suffix, attributes=function.get_passthrough_resource_attributes()) try: # Name will not be available for Alias resources @@ -138,12 +139,13 @@ def to_cloudformation(self, **kwargs): resources = [] events_rule = EventsRule(self.logical_id) - resources.append(events_rule) - events_rule.EventPattern = self.Pattern events_rule.Targets = [self._construct_target(function)] - source_arn = events_rule.get_runtime_attr("arn") + if 'Condition' in function.resource_attributes: + events_rule = make_conditional(events_rule, function.resource_attributes['Condition']) + resources.append(events_rule) + source_arn = events_rule.get_runtime_attr("arn") resources.append(self._construct_permission(function, source_arn=source_arn)) return resources @@ -210,7 +212,10 @@ def to_cloudformation(self, **kwargs): source_account = ref('AWS::AccountId') permission = self._construct_permission(function, source_account=source_account) - self._depend_on_lambda_permissions(bucket, permission) + if 'Condition' in permission.resource_attributes: + self._depend_on_lambda_permissions_using_tag(bucket, permission) + else: + self._depend_on_lambda_permissions(bucket, permission) resources.append(permission) # NOTE: `bucket` here is a dictionary representing the S3 Bucket resource in your SAM template. If there are @@ -253,6 +258,31 @@ def _depend_on_lambda_permissions(self, bucket, permission): return bucket + def _depend_on_lambda_permissions_using_tag(self, bucket, permission): + """ + Since conditional DependsOn is not supported this undocumented way of + implicitely making dependency through tags is used. + """ + properties = bucket.get('Properties', None) + if properties is None: + properties = {} + bucket['Properties'] = properties + tags = properties.get('Tags', None) + if tags is None: + tags = [] + properties['Tags'] = tags + dep_tag = { + 'sam:ConditionalDependsOn:' + permission.logical_id: { + 'Fn:If': [ + permission.resource_attributes['Condition'], + fnGetAtt(permission.logical_id, 'Arn'), + 'no dpendency' + ] + } + } + properties['Tags'] = tags + get_tag_list(dep_tag) + return bucket + def _inject_notification_configuration(self, function, bucket): base_event_mapping = { 'Function': function.get_runtime_attr("arn") @@ -270,7 +300,8 @@ def _inject_notification_configuration(self, function, bucket): lambda_event = copy.deepcopy(base_event_mapping) lambda_event['Event'] = event_type - + if 'Condition' in function.resource_attributes: + lambda_event = make_conditional(lambda_event, function.resource_attributes['Condition']) event_mappings.append(lambda_event) properties = bucket.get('Properties', None) diff --git a/samtranslator/model/intrinsics.py b/samtranslator/model/intrinsics.py index fd5e8e0e7..64e43d973 100644 --- a/samtranslator/model/intrinsics.py +++ b/samtranslator/model/intrinsics.py @@ -15,6 +15,14 @@ def fnSub(string, variables=None): return {'Fn::Sub': [string, variables]} return {'Fn::Sub': string} +def make_conditional(condition, data): + return { + 'Fn::If': [ + condition, + data, + { 'Ref': 'AWS::NoValue' } + ] + } def make_shorthand(intrinsic_dict): """ diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index a57cbc733..e099bab12 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -202,7 +202,7 @@ def _construct_role(self, managed_policy_map): :returns: the generated IAM Role :rtype: model.iam.IAMRole """ - execution_role = IAMRole(self.logical_id + 'Role') + execution_role = IAMRole(self.logical_id + 'Role', attributes=self.get_passthrough_resource_attributes()) execution_role.AssumeRolePolicyDocument = IAMRolePolicies.lambda_assume_role_policy() managed_policy_arns = [ArnGenerator.generate_aws_managed_policy_arn('service-role/AWSLambdaBasicExecutionRole')] @@ -399,11 +399,12 @@ def _construct_version(self, function, intrinsics_resolver): prefix = "{id}Version".format(id=self.logical_id) logical_id = logical_id_generator.LogicalIdGenerator(prefix, code_dict).gen() - retain_old_versions = { - "DeletionPolicy": "Retain" - } + attributes = self.get_passthrough_resource_attributes() + if attributes is None: + attributes = {} + attributes["DeletionPolicy"] = "Retain" - lambda_version = LambdaVersion(logical_id=logical_id, attributes=retain_old_versions) + lambda_version = LambdaVersion(logical_id=logical_id, attributes=attributes) lambda_version.FunctionName = function.get_runtime_attr('name') return lambda_version @@ -422,7 +423,7 @@ def _construct_alias(self, name, function, version): raise ValueError("Alias name is required to create an alias") logical_id = "{id}Alias{suffix}".format(id=function.logical_id, suffix=name) - alias = LambdaAlias(logical_id=logical_id) + alias = LambdaAlias(logical_id=logical_id, attributes=self.get_passthrough_resource_attributes()) alias.Name = name alias.FunctionName = function.get_runtime_attr('name') alias.FunctionVersion = version.get_runtime_attr("version") diff --git a/tests/model/eventsources/test_cloudwatchlogs_event_source.py b/tests/model/eventsources/test_cloudwatchlogs_event_source.py index 22d760a1a..f49cb7975 100644 --- a/tests/model/eventsources/test_cloudwatchlogs_event_source.py +++ b/tests/model/eventsources/test_cloudwatchlogs_event_source.py @@ -14,6 +14,7 @@ def setUp(self): self.function = Mock() self.function.get_runtime_attr = Mock() self.function.get_runtime_attr.return_value = 'arn:aws:mock' + self.function.get_passthrough_resource_attributes.return_value = None self.permission = Mock() self.permission.logical_id = 'LogProcessorPermission' diff --git a/tests/translator/input/s3_with_condition.yaml b/tests/translator/input/s3_with_condition.yaml new file mode 100644 index 000000000..e0f4e7988 --- /dev/null +++ b/tests/translator/input/s3_with_condition.yaml @@ -0,0 +1,18 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Condition: MyCondition + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket diff --git a/tests/translator/output/aws-cn/function_with_condition.json b/tests/translator/output/aws-cn/function_with_condition.json index dc6e0bca2..543e1445c 100644 --- a/tests/translator/output/aws-cn/function_with_condition.json +++ b/tests/translator/output/aws-cn/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/aws-cn/s3_with_condition.json b/tests/translator/output/aws-cn/s3_with_condition.json new file mode 100644 index 000000000..b0495b3c9 --- /dev/null +++ b/tests/translator/output/aws-cn/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_condition.json b/tests/translator/output/aws-us-gov/function_with_condition.json index c2d22acf1..7f7c2858d 100644 --- a/tests/translator/output/aws-us-gov/function_with_condition.json +++ b/tests/translator/output/aws-us-gov/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/aws-us-gov/s3_with_condition.json b/tests/translator/output/aws-us-gov/s3_with_condition.json new file mode 100644 index 000000000..d16c47f68 --- /dev/null +++ b/tests/translator/output/aws-us-gov/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_condition.json b/tests/translator/output/function_with_condition.json index fc7af1fde..e20488c63 100644 --- a/tests/translator/output/function_with_condition.json +++ b/tests/translator/output/function_with_condition.json @@ -26,6 +26,7 @@ }, "ConditionFunctionRole": { "Type": "AWS::IAM::Role", + "Condition": "this is a test", "Properties": { "ManagedPolicyArns": [ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/s3_with_condition.json b/tests/translator/output/s3_with_condition.json new file mode 100644 index 000000000..e429cab24 --- /dev/null +++ b/tests/translator/output/s3_with_condition.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Fn::If": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + }, + "MyCondition", + { + "Ref": "AWS::NoValue" + } + ] + } + ] + }, + "Tags": [ + { + "Value": { + "Fn:If": [ + "MyCondition", + { + "Fn::GetAtt": [ + "ThumbnailFunctionImageBucketPermission", + "Arn" + ] + }, + "no dpendency" + ] + }, + "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" + } + ] + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Condition": "MyCondition", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Condition": "MyCondition", + "Properties": { + "Action": "lambda:invokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Condition": "MyCondition", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 39d19c69b..a121c4e9b 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -93,6 +93,7 @@ class TestTranslatorEndToEnd(TestCase): @parameterized.expand( itertools.product([ + 's3_with_condition', 'function_with_condition', 'basic_function', 'cloudwatchevent', From 1b9e5b8243607c8a9991adb972c3373fc9767b31 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sun, 11 Nov 2018 12:25:25 +0100 Subject: [PATCH 11/14] DynamoDB table support for conditionals Push event conditionals --- samtranslator/model/eventsources/push.py | 4 ++++ samtranslator/model/sam_resources.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 02d0ef23f..8e7202e9e 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -354,6 +354,8 @@ def _inject_subscription(self, function, topic, filterPolicy): subscription.Protocol = 'lambda' subscription.Endpoint = function.get_runtime_attr("arn") subscription.TopicArn = topic + if 'Condition' in function.resource_attributes: + subscription = make_conditional(subscription, function.resource_attributes['Condition']) if filterPolicy is not None: subscription.FilterPolicy = filterPolicy @@ -610,5 +612,7 @@ def _construct_iot_rule(self, function): payload['AwsIotSqlVersion'] = self.AwsIotSqlVersion rule.TopicRulePayload = payload + if 'Condition' in function.resource_attributes: + rule = make_conditional(rule, function.resource_attributes['Condition']) return rule diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index e099bab12..6acfb238f 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -547,7 +547,7 @@ def to_cloudformation(self, **kwargs): return [dynamodb_resources] def _construct_dynamodb_table(self): - dynamodb_table = DynamoDBTable(self.logical_id, depends_on=self.depends_on) + dynamodb_table = DynamoDBTable(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) if self.PrimaryKey: primary_key = { From 499b2e721fc393d73321a4b92c586b87d9f96853 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sat, 24 Nov 2018 14:40:50 +0100 Subject: [PATCH 12/14] Fixes for review --- samtranslator/model/eventsources/push.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 8e7202e9e..fa4ef8cbb 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -262,6 +262,12 @@ def _depend_on_lambda_permissions_using_tag(self, bucket, permission): """ Since conditional DependsOn is not supported this undocumented way of implicitely making dependency through tags is used. + + See https://stackoverflow.com/questions/34607476/cloudformation-apply-condition-on-dependson + + It is done by using Fn:GetAtt wrapped in a conditional Fn:If. Using Fn:GetAtt implies a + dependency, so CloudFormation will automatically wait once it reaches that function, the same + as if you were using a DependsOn. """ properties = bucket.get('Properties', None) if properties is None: @@ -276,7 +282,7 @@ def _depend_on_lambda_permissions_using_tag(self, bucket, permission): 'Fn:If': [ permission.resource_attributes['Condition'], fnGetAtt(permission.logical_id, 'Arn'), - 'no dpendency' + 'no dependency' ] } } From c88d36fbf3b0b8935291707244d60e36758e6319 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sun, 25 Nov 2018 07:39:50 +0100 Subject: [PATCH 13/14] Type in test outputs --- tests/translator/output/aws-cn/s3_with_condition.json | 2 +- tests/translator/output/aws-us-gov/s3_with_condition.json | 2 +- tests/translator/output/s3_with_condition.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/translator/output/aws-cn/s3_with_condition.json b/tests/translator/output/aws-cn/s3_with_condition.json index b0495b3c9..1e63b9d64 100644 --- a/tests/translator/output/aws-cn/s3_with_condition.json +++ b/tests/translator/output/aws-cn/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" diff --git a/tests/translator/output/aws-us-gov/s3_with_condition.json b/tests/translator/output/aws-us-gov/s3_with_condition.json index d16c47f68..4d5df8b17 100644 --- a/tests/translator/output/aws-us-gov/s3_with_condition.json +++ b/tests/translator/output/aws-us-gov/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" diff --git a/tests/translator/output/s3_with_condition.json b/tests/translator/output/s3_with_condition.json index e429cab24..7a4e8a393 100644 --- a/tests/translator/output/s3_with_condition.json +++ b/tests/translator/output/s3_with_condition.json @@ -35,7 +35,7 @@ "Arn" ] }, - "no dpendency" + "no dependency" ] }, "Key": "sam:ConditionalDependsOn:ThumbnailFunctionImageBucketPermission" From 03b4e1cf311824cd5176d721051493c0c7bd75d6 Mon Sep 17 00:00:00 2001 From: Jacco Kulman Date: Sun, 25 Nov 2018 08:24:48 +0100 Subject: [PATCH 14/14] Fix mocks --- tests/model/eventsources/test_cloudwatchlogs_event_source.py | 4 +++- tests/model/eventsources/test_sns_event_source.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/model/eventsources/test_cloudwatchlogs_event_source.py b/tests/model/eventsources/test_cloudwatchlogs_event_source.py index f49cb7975..cd58d98af 100644 --- a/tests/model/eventsources/test_cloudwatchlogs_event_source.py +++ b/tests/model/eventsources/test_cloudwatchlogs_event_source.py @@ -14,7 +14,9 @@ def setUp(self): self.function = Mock() self.function.get_runtime_attr = Mock() self.function.get_runtime_attr.return_value = 'arn:aws:mock' - self.function.get_passthrough_resource_attributes.return_value = None + self.function.resource_attributes = {} + self.function.get_passthrough_resource_attributes = Mock() + self.function.get_passthrough_resource_attributes.return_value = {} self.permission = Mock() self.permission.logical_id = 'LogProcessorPermission' diff --git a/tests/model/eventsources/test_sns_event_source.py b/tests/model/eventsources/test_sns_event_source.py index abe7eda4d..2836d9d89 100644 --- a/tests/model/eventsources/test_sns_event_source.py +++ b/tests/model/eventsources/test_sns_event_source.py @@ -14,6 +14,9 @@ def setUp(self): self.function = Mock() self.function.get_runtime_attr = Mock() self.function.get_runtime_attr.return_value = 'arn:aws:lambda:mock' + self.function.resource_attributes = {} + self.function.get_passthrough_resource_attributes = Mock() + self.function.get_passthrough_resource_attributes.return_value = {} def test_to_cloudformation_returns_permission_and_subscription_resources(self): resources = self.sns_event_source.to_cloudformation(