Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v1.36.0 #2022

Merged
merged 16 commits into from
May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ environment that lets you locally build, test, debug, and deploy applications de

## What is this Github repository? 💻

This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of examples applications.
This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of example applications.
In the words of SAM developers:

> SAM Translator is the Python code that deploys SAM templates via AWS CloudFormation. Source code is high quality (95% unit test coverage),
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/generated_resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ AWS::Lambda::Permission MyFunction\ **ThumbnailApi**\ Permission\ **P
NOTE: ``ServerlessRestApi*`` resources are generated one per stack.

HTTP API
^^^
^^^^
This is called an "Implicit HTTP API". There can be many functions in the template that define these APIs. Behind the
scenes, SAM will collect all implicit HTTP APIs from all Functions in the template, generate an OpenApi doc, and create an
implicit ``AWS::Serverless::HttpApi`` using this OpenApi. This API defaults to a StageName called "$default" that cannot be
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ flake8~=3.8.4
tox~=3.20.1
pytest-cov~=2.10.1
pylint>=1.7.2,<2.0
pyyaml~=5.3.1
pyyaml~=5.4

# Test requirements
pytest~=6.1.1; python_version >= '3.6'
Expand Down
2 changes: 1 addition & 1 deletion samtranslator/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.35.0"
__version__ = "1.36.0"
4 changes: 3 additions & 1 deletion samtranslator/intrinsics/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class IntrinsicsResolver(object):
def __init__(self, parameters, supported_intrinsics=DEFAULT_SUPPORTED_INTRINSICS):
def __init__(self, parameters, supported_intrinsics=None):
"""
Instantiate the resolver
:param dict parameters: Map of parameter names to their values
Expand All @@ -17,6 +17,8 @@ def __init__(self, parameters, supported_intrinsics=DEFAULT_SUPPORTED_INTRINSICS
:raises TypeError: If parameters or the supported_intrinsics arguments are invalid
"""

if supported_intrinsics is None:
supported_intrinsics = DEFAULT_SUPPORTED_INTRINSICS
if parameters is None or not isinstance(parameters, dict):
raise InvalidDocumentException(
[InvalidTemplateException("'Mappings' or 'Parameters' is either null or not a valid dictionary.")]
Expand Down
29 changes: 25 additions & 4 deletions samtranslator/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ class Resource(object):
property_types = None
_keywords = ["logical_id", "relative_id", "depends_on", "resource_attributes"]

_supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition"]
# For attributes in this list, they will be passed into the translated template for the same resource itself.
_supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition", "UpdateReplacePolicy", "Metadata"]
# For attributes in this list, they will be passed into the translated template for the same resource,
# as well as all the auto-generated resources that are created from this resource.
_pass_through_attributes = ["Condition", "DeletionPolicy", "UpdateReplacePolicy"]

# 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
Expand Down Expand Up @@ -76,6 +80,22 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non
for attr, value in attributes.items():
self.set_resource_attribute(attr, value)

@classmethod
def get_supported_resource_attributes(cls):
"""
A getter method for the supported resource attributes
returns: a tuple that contains the name of all supported resource attributes
"""
return tuple(cls._supported_resource_attributes)

@classmethod
def get_pass_through_attributes(cls):
"""
A getter method for the resource attributes to be passed to auto-generated resources
returns: a tuple that contains the name of all pass through attributes
"""
return tuple(cls._pass_through_attributes)

@classmethod
def from_dict(cls, logical_id, resource_dict, relative_id=None, sam_plugins=None):
"""Constructs a Resource object with the given logical id, based on the given resource dict. The resource dict
Expand Down Expand Up @@ -318,9 +338,10 @@ def get_passthrough_resource_attributes(self):

:return: Dictionary of resource attributes.
"""
attributes = None
if "Condition" in self.resource_attributes:
attributes = {"Condition": self.resource_attributes["Condition"]}
attributes = {}
for resource_attribute in self.get_pass_through_attributes():
if resource_attribute in self.resource_attributes:
attributes[resource_attribute] = self.resource_attributes.get(resource_attribute)
return attributes


Expand Down
71 changes: 53 additions & 18 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from collections import namedtuple
from six import string_types
from samtranslator.model.intrinsics import ref, fnGetAtt
Expand All @@ -24,6 +25,8 @@
from samtranslator.translator.arn_generator import ArnGenerator
from samtranslator.model.tags.resource_tagging import get_tag_list

LOG = logging.getLogger(__name__)

_CORS_WILDCARD = "'*'"
CorsProperties = namedtuple(
"_CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigin", "MaxAge", "AllowCredentials"]
Expand Down Expand Up @@ -52,12 +55,20 @@
GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]


class ApiGenerator(object):
usage_plan_shared = False
stage_keys_shared = list()
api_stages_shared = list()
depends_on_shared = list()
class SharedApiUsagePlan(object):
"""
Collects API information from different API resources in the same template,
so that these information can be used in the shared usage plan
"""

def __init__(self):
self.usage_plan_shared = False
self.stage_keys_shared = list()
self.api_stages_shared = list()
self.depends_on_shared = list()


class ApiGenerator(object):
def __init__(
self,
logical_id,
Expand All @@ -69,6 +80,7 @@ def __init__(
definition_uri,
name,
stage_name,
shared_api_usage_plan,
tags=None,
endpoint_configuration=None,
method_settings=None,
Expand Down Expand Up @@ -134,6 +146,7 @@ def __init__(
self.models = models
self.domain = domain
self.description = description
self.shared_api_usage_plan = shared_api_usage_plan

def _construct_rest_api(self):
"""Constructs and returns the ApiGateway RestApi.
Expand Down Expand Up @@ -617,7 +630,11 @@ def _construct_usage_plan(self, rest_api_stage=None):
# create usage plan for this api only
elif usage_plan_properties.get("CreateUsagePlan") == "PER_API":
usage_plan_logical_id = self.logical_id + "UsagePlan"
usage_plan = ApiGatewayUsagePlan(logical_id=usage_plan_logical_id, depends_on=[self.logical_id])
usage_plan = ApiGatewayUsagePlan(
logical_id=usage_plan_logical_id,
depends_on=[self.logical_id],
attributes=self.passthrough_resource_attributes,
)
api_stages = list()
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
Expand All @@ -630,18 +647,21 @@ def _construct_usage_plan(self, rest_api_stage=None):

# create a usage plan for all the Apis
elif create_usage_plan == "SHARED":
LOG.info("Creating SHARED usage plan for all the Apis")
usage_plan_logical_id = "ServerlessUsagePlan"
if self.logical_id not in ApiGenerator.depends_on_shared:
ApiGenerator.depends_on_shared.append(self.logical_id)
if self.logical_id not in self.shared_api_usage_plan.depends_on_shared:
self.shared_api_usage_plan.depends_on_shared.append(self.logical_id)
usage_plan = ApiGatewayUsagePlan(
logical_id=usage_plan_logical_id, depends_on=ApiGenerator.depends_on_shared
logical_id=usage_plan_logical_id,
depends_on=self.shared_api_usage_plan.depends_on_shared,
attributes=self.passthrough_resource_attributes,
)
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
api_stage["Stage"] = ref(rest_api_stage.logical_id)
if api_stage not in ApiGenerator.api_stages_shared:
ApiGenerator.api_stages_shared.append(api_stage)
usage_plan.ApiStages = ApiGenerator.api_stages_shared
if api_stage not in self.shared_api_usage_plan.api_stages_shared:
self.shared_api_usage_plan.api_stages_shared.append(api_stage)
usage_plan.ApiStages = self.shared_api_usage_plan.api_stages_shared

api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)
Expand All @@ -667,20 +687,30 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
"""
if create_usage_plan == "SHARED":
# create an api key resource for all the apis
LOG.info("Creating api key resource for all the Apis from SHARED usage plan")
api_key_logical_id = "ServerlessApiKey"
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key = ApiGatewayApiKey(
logical_id=api_key_logical_id,
depends_on=[usage_plan_logical_id],
attributes=self.passthrough_resource_attributes,
)
api_key.Enabled = True
stage_key = dict()
stage_key["RestApiId"] = ref(self.logical_id)
stage_key["StageName"] = ref(rest_api_stage.logical_id)
if stage_key not in ApiGenerator.stage_keys_shared:
ApiGenerator.stage_keys_shared.append(stage_key)
api_key.StageKeys = ApiGenerator.stage_keys_shared
if stage_key not in self.shared_api_usage_plan.stage_keys_shared:
self.shared_api_usage_plan.stage_keys_shared.append(stage_key)
api_key.StageKeys = self.shared_api_usage_plan.stage_keys_shared
# for create_usage_plan = "PER_API"
else:
# create an api key resource for this api
api_key_logical_id = self.logical_id + "ApiKey"
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key = ApiGatewayApiKey(
logical_id=api_key_logical_id,
depends_on=[usage_plan_logical_id],
attributes=self.passthrough_resource_attributes,
)
# api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key.Enabled = True
stage_keys = list()
stage_key = dict()
Expand All @@ -705,7 +735,12 @@ def _construct_usage_plan_key(self, usage_plan_logical_id, create_usage_plan, ap
# create a mapping between api key and the usage plan
usage_plan_key_logical_id = self.logical_id + "UsagePlanKey"

usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id])
usage_plan_key = ApiGatewayUsagePlanKey(
logical_id=usage_plan_key_logical_id,
depends_on=[api_key.logical_id],
attributes=self.passthrough_resource_attributes,
)
# usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id])
usage_plan_key.KeyId = ref(api_key.logical_id)
usage_plan_key.KeyType = "API_KEY"
usage_plan_key.UsagePlanId = ref(usage_plan_logical_id)
Expand Down
11 changes: 8 additions & 3 deletions samtranslator/model/apigateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,10 @@ def __init__(
function_payload_type=None,
function_invoke_role=None,
is_aws_iam_authorizer=False,
authorization_scopes=[],
authorization_scopes=None,
):
if authorization_scopes is None:
authorization_scopes = []
if function_payload_type not in ApiGatewayAuthorizer._VALID_FUNCTION_PAYLOAD_TYPES:
raise InvalidResourceException(
api_logical_id,
Expand Down Expand Up @@ -267,8 +269,9 @@ def _is_missing_identity_source(self, identity):
query_strings = identity.get("QueryStrings")
stage_variables = identity.get("StageVariables")
context = identity.get("Context")
ttl = identity.get("ReauthorizeEvery")

if not headers and not query_strings and not stage_variables and not context:
if (ttl is None or int(ttl) > 0) and not headers and not query_strings and not stage_variables and not context:
return True

return False
Expand Down Expand Up @@ -311,7 +314,9 @@ def generate_swagger(self):
swagger[APIGATEWAY_AUTHORIZER_KEY]["authorizerCredentials"] = function_invoke_role

if self._get_function_payload_type() == "REQUEST":
swagger[APIGATEWAY_AUTHORIZER_KEY]["identitySource"] = self._get_identity_source()
identity_source = self._get_identity_source()
if identity_source:
swagger[APIGATEWAY_AUTHORIZER_KEY]["identitySource"] = self._get_identity_source()

# Authorizer Validation Expression is only allowed on COGNITO_USER_POOLS and LAMBDA_TOKEN
is_lambda_token_authorizer = authorizer_type == "LAMBDA" and self._get_function_payload_type() == "TOKEN"
Expand Down
10 changes: 5 additions & 5 deletions samtranslator/model/eventbridge_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

class EventBridgeRuleUtils:
@staticmethod
def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None):
def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None, attributes=None):
resources = []

queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue")
queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue", attributes=attributes)
dlq_queue_arn = queue.get_runtime_attr("arn")
dlq_queue_url = queue.get_runtime_attr("queue_url")

# grant necessary permission to Eventbridge Rule resource for sending messages to dead-letter queue
policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy")
policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy", attributes=attributes)
policy.PolicyDocument = SQSQueuePolicies.eventbridge_dlq_send_message_resource_based_policy(
rule_arn, dlq_queue_arn
)
Expand Down Expand Up @@ -41,14 +41,14 @@ def validate_dlq_config(source_logical_id, dead_letter_config):
raise InvalidEventException(source_logical_id, "No 'Arn' or 'Type' property provided for DeadLetterConfig")

@staticmethod
def get_dlq_queue_arn_and_resources(cw_event_source, source_arn):
def get_dlq_queue_arn_and_resources(cw_event_source, source_arn, attributes):
"""returns dlq queue arn and dlq_resources, assuming cw_event_source.DeadLetterConfig has been validated"""
dlq_queue_arn = cw_event_source.DeadLetterConfig.get("Arn")
if dlq_queue_arn is not None:
return dlq_queue_arn, []
queue_logical_id = cw_event_source.DeadLetterConfig.get("QueueLogicalId")
dlq_resources = EventBridgeRuleUtils.create_dead_letter_queue_with_policy(
cw_event_source.logical_id, source_arn, queue_logical_id
cw_event_source.logical_id, source_arn, queue_logical_id, attributes
)
dlq_queue_arn = dlq_resources[0].get_runtime_attr("arn")
return dlq_queue_arn, dlq_resources
8 changes: 5 additions & 3 deletions samtranslator/model/eventsources/cloudwatchlogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ def get_source_arn(self):
)

def get_subscription_filter(self, function, permission):
subscription_filter = SubscriptionFilter(self.logical_id, depends_on=[permission.logical_id])
subscription_filter = SubscriptionFilter(
self.logical_id,
depends_on=[permission.logical_id],
attributes=function.get_passthrough_resource_attributes(),
)
subscription_filter.LogGroupName = self.LogGroupName
subscription_filter.FilterPattern = self.FilterPattern
subscription_filter.DestinationArn = function.get_runtime_attr("arn")
if "Condition" in function.resource_attributes:
subscription_filter.set_resource_attribute("Condition", function.resource_attributes["Condition"])

return subscription_filter
7 changes: 3 additions & 4 deletions samtranslator/model/eventsources/pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ def to_cloudformation(self, **kwargs):

resources = []

lambda_eventsourcemapping = LambdaEventSourceMapping(self.logical_id)
lambda_eventsourcemapping = LambdaEventSourceMapping(
self.logical_id, attributes=function.get_passthrough_resource_attributes()
)
resources.append(lambda_eventsourcemapping)

try:
Expand Down Expand Up @@ -122,9 +124,6 @@ def to_cloudformation(self, **kwargs):
)
lambda_eventsourcemapping.DestinationConfig = self.DestinationConfig

if "Condition" in function.resource_attributes:
lambda_eventsourcemapping.set_resource_attribute("Condition", function.resource_attributes["Condition"])

if "role" in kwargs:
self._link_policy(kwargs["role"], destination_config_policy)

Expand Down
Loading