From f0fc16bd9d908791be49b080c5b0ef417a5f98ec Mon Sep 17 00:00:00 2001 From: Slava Senchenko Date: Mon, 14 Nov 2022 12:16:55 -0800 Subject: [PATCH 1/4] RedrivePolicy added (#2583) Co-authored-by: Slava Senchenko --- samtranslator/model/eventsources/push.py | 27 +++++++++++++++---- samtranslator/model/sns.py | 1 + .../eventsources/test_sns_event_source.py | 12 +++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index b3f9a7177..4e4d98954 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1,5 +1,6 @@ import copy import re +from typing import Any, Dict, Optional from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model import ResourceMacro, PropertyType @@ -451,6 +452,7 @@ class SNS(PushEventSource): "Region": PropertyType(False, is_str()), "FilterPolicy": PropertyType(False, dict_of(is_str(), list_of(one_of(is_str(), is_type(dict))))), "SqsSubscription": PropertyType(False, one_of(is_type(bool), is_type(dict))), + "RedrivePolicy": PropertyType(False, is_type(dict)), } @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] @@ -469,12 +471,13 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # SNS -> Lambda if not self.SqsSubscription: # type: ignore[attr-defined] - subscription = self._inject_subscription( # type: ignore[no-untyped-call] + subscription = self._inject_subscription( "lambda", function.get_runtime_attr("arn"), self.Topic, # type: ignore[attr-defined] self.Region, # type: ignore[attr-defined] self.FilterPolicy, # type: ignore[attr-defined] + self.RedrivePolicy, # type: ignore[attr-defined] function, ) return [self._construct_permission(function, source_arn=self.Topic), subscription] # type: ignore[attr-defined, no-untyped-call] @@ -487,8 +490,8 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] queue_url = queue.get_runtime_attr("queue_url") queue_policy = self._inject_sqs_queue_policy(self.Topic, queue_arn, queue_url, function) # type: ignore[attr-defined, no-untyped-call] - subscription = self._inject_subscription( # type: ignore[no-untyped-call] - "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function # type: ignore[attr-defined, attr-defined, attr-defined] + subscription = self._inject_subscription( + "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, self.RedrivePolicy, function # type: ignore[attr-defined, attr-defined, attr-defined] ) event_source = self._inject_sqs_event_source_mapping(function, role, queue_arn) # type: ignore[no-untyped-call] @@ -512,7 +515,9 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] queue_policy = self._inject_sqs_queue_policy( # type: ignore[no-untyped-call] self.Topic, queue_arn, queue_url, function, queue_policy_logical_id # type: ignore[attr-defined] ) - subscription = self._inject_subscription("sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function) # type: ignore[attr-defined, attr-defined, attr-defined, no-untyped-call] + subscription = self._inject_subscription( + "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, self.RedrivePolicy, function # type: ignore[attr-defined, attr-defined, attr-defined] + ) event_source = self._inject_sqs_event_source_mapping(function, role, queue_arn, batch_size, enabled) # type: ignore[no-untyped-call] resources = resources + event_source @@ -520,7 +525,16 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] resources.append(subscription) return resources - def _inject_subscription(self, protocol, endpoint, topic, region, filterPolicy, function): # type: ignore[no-untyped-def] + def _inject_subscription( + self, + protocol: str, + endpoint: str, + topic: str, + region: Optional[str], + filterPolicy: Optional[Dict[str, Any]], + redrivePolicy: Optional[Dict[str, Any]], + function: Any, + ) -> SNSSubscription: subscription = SNSSubscription(self.logical_id, attributes=function.get_passthrough_resource_attributes()) subscription.Protocol = protocol subscription.Endpoint = endpoint @@ -532,6 +546,9 @@ def _inject_subscription(self, protocol, endpoint, topic, region, filterPolicy, if filterPolicy is not None: subscription.FilterPolicy = filterPolicy + if redrivePolicy is not None: + subscription.RedrivePolicy = redrivePolicy + return subscription def _inject_sqs_queue(self, function): # type: ignore[no-untyped-def] diff --git a/samtranslator/model/sns.py b/samtranslator/model/sns.py index a1531849f..c87b2f6da 100644 --- a/samtranslator/model/sns.py +++ b/samtranslator/model/sns.py @@ -11,6 +11,7 @@ class SNSSubscription(Resource): "TopicArn": PropertyType(True, is_str()), "Region": PropertyType(False, is_str()), "FilterPolicy": PropertyType(False, is_type(dict)), + "RedrivePolicy": PropertyType(False, is_type(dict)), } diff --git a/tests/model/eventsources/test_sns_event_source.py b/tests/model/eventsources/test_sns_event_source.py index 1bd72516f..1af05f78b 100644 --- a/tests/model/eventsources/test_sns_event_source.py +++ b/tests/model/eventsources/test_sns_event_source.py @@ -29,6 +29,7 @@ def test_to_cloudformation_returns_permission_and_subscription_resources(self): self.assertEqual(subscription.Endpoint, "arn:aws:lambda:mock") self.assertIsNone(subscription.Region) self.assertIsNone(subscription.FilterPolicy) + self.assertIsNone(subscription.RedrivePolicy) def test_to_cloudformation_passes_the_region(self): region = "us-west-2" @@ -54,6 +55,16 @@ def test_to_cloudformation_passes_the_filter_policy(self): subscription = resources[1] self.assertEqual(subscription.FilterPolicy, filterPolicy) + def test_to_cloudformation_passes_the_redrive_policy(self): + redrive_policy = {"deadLetterTargetArn": "arn:aws:sqs:us-east-2:123456789012:MyDeadLetterQueue"} + self.sns_event_source.RedrivePolicy = redrive_policy + + resources = self.sns_event_source.to_cloudformation(function=self.function) + self.assertEqual(len(resources), 2) + self.assertEqual(resources[1].resource_type, "AWS::SNS::Subscription") + subscription = resources[1] + self.assertEqual(subscription.RedrivePolicy, redrive_policy) + def test_to_cloudformation_throws_when_no_function(self): self.assertRaises(TypeError, self.sns_event_source.to_cloudformation) @@ -77,3 +88,4 @@ def test_to_cloudformation_when_sqs_subscription_disable(self): self.assertEqual(subscription.Endpoint, "arn:aws:lambda:mock") self.assertIsNone(subscription.Region) self.assertIsNone(subscription.FilterPolicy) + self.assertIsNone(subscription.RedrivePolicy) From 500743724b5c312de2224171825bf5df7265e39c Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 14 Nov 2022 15:42:25 -0800 Subject: [PATCH 2/4] chore: Type improvements in http api generator (#2586) --- .../feature_toggle/feature_toggle.py | 2 +- samtranslator/metrics/method_decorator.py | 9 +- samtranslator/model/__init__.py | 4 +- samtranslator/model/api/api_generator.py | 18 +- samtranslator/model/api/http_api_generator.py | 309 ++++++++++-------- samtranslator/model/apigateway.py | 4 +- samtranslator/model/apigatewayv2.py | 14 +- .../model/eventsources/cloudwatchlogs.py | 2 +- samtranslator/model/eventsources/pull.py | 2 +- samtranslator/model/eventsources/push.py | 28 +- samtranslator/model/eventsources/scheduler.py | 2 +- samtranslator/model/route53.py | 7 + samtranslator/model/s3_utils/uri_parser.py | 8 +- samtranslator/model/sam_resources.py | 14 +- samtranslator/model/stepfunctions/events.py | 10 +- .../model/stepfunctions/generators.py | 9 +- samtranslator/model/tags/resource_tagging.py | 4 +- samtranslator/open_api/open_api.py | 28 +- .../api/default_definition_body_plugin.py | 2 +- .../plugins/api/implicit_api_plugin.py | 2 +- .../application/serverless_app_plugin.py | 8 +- .../plugins/globals/globals_plugin.py | 2 +- .../policies/policy_templates_plugin.py | 2 +- .../translator/logical_id_generator.py | 5 +- .../translator/managed_policy_translator.py | 2 +- samtranslator/utils/types.py | 6 + ...tpapi_mtls_configuration_invalid_type.json | 4 +- 27 files changed, 296 insertions(+), 211 deletions(-) create mode 100644 samtranslator/utils/types.py diff --git a/samtranslator/feature_toggle/feature_toggle.py b/samtranslator/feature_toggle/feature_toggle.py index efecb58da..c0f0a5f0c 100644 --- a/samtranslator/feature_toggle/feature_toggle.py +++ b/samtranslator/feature_toggle/feature_toggle.py @@ -125,7 +125,7 @@ def config(self): # type: ignore[no-untyped-def] class FeatureToggleAppConfigConfigProvider(FeatureToggleConfigProvider): """Feature toggle config provider which loads config from AppConfig.""" - @cw_timer(prefix="External", name="AppConfig") # type: ignore[no-untyped-call] + @cw_timer(prefix="External", name="AppConfig") # type: ignore[misc] def __init__(self, application_id, environment_id, configuration_profile_id, app_config_client=None): # type: ignore[no-untyped-def] FeatureToggleConfigProvider.__init__(self) try: diff --git a/samtranslator/metrics/method_decorator.py b/samtranslator/metrics/method_decorator.py index ac367a934..fbe09328c 100644 --- a/samtranslator/metrics/method_decorator.py +++ b/samtranslator/metrics/method_decorator.py @@ -6,6 +6,7 @@ from samtranslator.metrics.metrics import DummyMetricsPublisher, Metrics from samtranslator.model import Resource import logging +from typing import Any, Callable, Optional, Union LOG = logging.getLogger(__name__) @@ -74,7 +75,9 @@ def _send_cw_metric(prefix, name, execution_time_ms, func, args): # type: ignor LOG.warning("Failed to add metrics", exc_info=e) -def cw_timer(_func=None, name=None, prefix=None): # type: ignore[no-untyped-def] +def cw_timer( + _func: Optional[Callable[..., Any]] = None, name: Optional[str] = None, prefix: Optional[str] = None +) -> Union[Callable[..., Any], Callable[[Callable[..., Any]], Callable[..., Any]]]: """ A method decorator, that will calculate execution time of the decorated method, and store this information as a metric in CloudWatch by calling the metrics singleton instance. @@ -87,7 +90,7 @@ def cw_timer(_func=None, name=None, prefix=None): # type: ignore[no-untyped-def If prefix is defined, it will be added in the beginning of what is been generated above """ - def cw_timer_decorator(func): # type: ignore[no-untyped-def] + def cw_timer_decorator(func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) def wrapper_cw_timer(*args, **kwargs): # type: ignore[no-untyped-def] start_time = datetime.now() @@ -102,4 +105,4 @@ def wrapper_cw_timer(*args, **kwargs): # type: ignore[no-untyped-def] return wrapper_cw_timer - return cw_timer_decorator if _func is None else cw_timer_decorator(_func) # type: ignore[no-untyped-call] + return cw_timer_decorator if _func is None else cw_timer_decorator(_func) diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index ba249e210..ee7d82ad8 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -376,7 +376,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] """ return {} - def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] + def to_cloudformation(self, **kwargs: Any) -> List[Any]: """Returns a list of Resource instances, representing vanilla CloudFormation resources, to which this macro expands. The caller should be able to update their template with the expanded resources by calling :func:`to_dict` on each resource returned, then updating their "Resources" mapping with the results. @@ -455,7 +455,7 @@ def _construct_tag_list(self, tags, additional_tags=None): # type: ignore[no-un # tags list. Changing this ordering will trigger a update on Lambda Function resource. Even though this # does not change the actual content of the tags, we don't want to trigger update of a resource without # customer's knowledge. - return get_tag_list(sam_tag) + get_tag_list(additional_tags) + get_tag_list(tags) # type: ignore[no-untyped-call] + return get_tag_list(sam_tag) + get_tag_list(additional_tags) + get_tag_list(tags) def _check_tag(self, reserved_tag_name, tags): # type: ignore[no-untyped-def] if reserved_tag_name in tags: diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 69a316367..778ff4824 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -338,15 +338,15 @@ def _construct_body_s3_dict(self): # type: ignore[no-untyped-def] s3_pointer = self.definition_uri else: - # DefinitionUri is a string - s3_pointer = parse_s3_uri(self.definition_uri) # type: ignore[no-untyped-call] - if s3_pointer is None: + _parsed_s3_pointer = parse_s3_uri(self.definition_uri) + if _parsed_s3_pointer is None: raise InvalidResourceException( self.logical_id, "'DefinitionUri' is not a valid S3 Uri of the form " "'s3://bucket/key' with optional versionId query parameter.", ) + s3_pointer = _parsed_s3_pointer if isinstance(self.definition_uri, Py27UniStr): # self.defintion_uri is a Py27UniStr instance if it is defined in the template @@ -394,8 +394,8 @@ def _construct_stage(self, deployment, swagger, redeploy_restapi_parameters): # if stage_name_prefix.isalnum(): stage_logical_id = self.logical_id + stage_name_prefix + "Stage" else: - generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) # type: ignore[no-untyped-call] - stage_logical_id = generator.gen() # type: ignore[no-untyped-call] + generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) + stage_logical_id = generator.gen() stage = ApiGatewayStage(stage_logical_id, attributes=self.passthrough_resource_attributes) stage.RestApiId = ref(self.logical_id) stage.update_deployment_ref(deployment.logical_id) # type: ignore[no-untyped-call] @@ -414,7 +414,7 @@ def _construct_stage(self, deployment, swagger, redeploy_restapi_parameters): # ) if self.tags is not None: - stage.Tags = get_tag_list(self.tags) # type: ignore[no-untyped-call] + stage.Tags = get_tag_list(self.tags) return stage @@ -432,7 +432,7 @@ def _construct_api_domain(self, rest_api, route53_record_set_groups): # type: i ) self.domain["ApiDomainName"] = "{}{}".format( - "ApiGatewayDomainName", LogicalIdGenerator("", self.domain.get("DomainName")).gen() # type: ignore[no-untyped-call, no-untyped-call] + "ApiGatewayDomainName", LogicalIdGenerator("", self.domain.get("DomainName")).gen() ) domain = ApiGatewayDomainName(self.domain.get("ApiDomainName"), attributes=self.passthrough_resource_attributes) @@ -537,7 +537,7 @@ def _construct_api_domain(self, rest_api, route53_record_set_groups): # type: i "HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.", ) - logical_id_suffix = LogicalIdGenerator( # type: ignore[no-untyped-call, no-untyped-call] + logical_id_suffix = LogicalIdGenerator( "", route53.get("HostedZoneId") or route53.get("HostedZoneName") ).gen() logical_id = "RecordSetGroup" + logical_id_suffix @@ -593,7 +593,7 @@ def _construct_alias_target(self, domain): # type: ignore[no-untyped-def] alias_target["DNSName"] = route53.get("DistributionDomainName") return alias_target - @cw_timer(prefix="Generator", name="Api") # type: ignore[no-untyped-call] + @cw_timer(prefix="Generator", name="Api") def to_cloudformation(self, redeploy_restapi_parameters, route53_record_set_groups): # type: ignore[no-untyped-def] """Generates CloudFormation resources from a SAM API resource diff --git a/samtranslator/model/api/http_api_generator.py b/samtranslator/model/api/http_api_generator.py index 22a8e9036..f0875c1cf 100644 --- a/samtranslator/model/api/http_api_generator.py +++ b/samtranslator/model/api/http_api_generator.py @@ -1,5 +1,6 @@ import re from collections import namedtuple +from typing import Any, Dict, List, Optional, Tuple, Union, cast from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model.intrinsics import ref, fnGetAtt @@ -16,6 +17,7 @@ from samtranslator.translator.logical_id_generator import LogicalIdGenerator from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_no_value from samtranslator.model.route53 import Route53RecordSetGroup +from samtranslator.utils.types import Intrinsicable _CORS_WILDCARD = "*" CorsProperties = namedtuple( @@ -30,27 +32,27 @@ class HttpApiGenerator(object): - def __init__( # type: ignore[no-untyped-def] + def __init__( self, - logical_id, - stage_variables, - depends_on, - definition_body, - definition_uri, - stage_name, - tags=None, - auth=None, - cors_configuration=None, - access_log_settings=None, - route_settings=None, - default_route_settings=None, - resource_attributes=None, - passthrough_resource_attributes=None, - domain=None, - fail_on_warnings=None, - description=None, - disable_execute_api_endpoint=None, - ): + logical_id: str, + stage_variables: Dict[str, Intrinsicable[str]], + depends_on: Optional[List[str]], + definition_body: Dict[str, Any], + definition_uri: Intrinsicable[str], + stage_name: Intrinsicable[str], + tags: Optional[Dict[str, Intrinsicable[str]]] = None, + auth: Optional[Dict[str, Intrinsicable[str]]] = None, + cors_configuration: Optional[Union[bool, Dict[str, Any]]] = None, + access_log_settings: Optional[Dict[str, Intrinsicable[str]]] = None, + route_settings: Optional[Dict[str, Any]] = None, + default_route_settings: Optional[Dict[str, Any]] = None, + resource_attributes: Optional[Dict[str, Intrinsicable[str]]] = None, + passthrough_resource_attributes: Optional[Dict[str, Intrinsicable[str]]] = None, + domain: Optional[Dict[str, Any]] = None, + fail_on_warnings: Optional[Intrinsicable[bool]] = None, + description: Optional[Intrinsicable[str]] = None, + disable_execute_api_endpoint: Optional[Intrinsicable[bool]] = None, + ) -> None: """Constructs an API Generator class that generates API Gateway resources :param logical_id: Logical id of the SAM API Resource @@ -58,7 +60,6 @@ def __init__( # type: ignore[no-untyped-def] :param depends_on: Any resources that need to be depended on :param definition_body: API definition :param definition_uri: URI to API definition - :param name: Name of the API Gateway resource :param stage_name: Name of the Stage :param tags: Stage and API Tags :param access_log_settings: Whether to send access logs and where for Stage @@ -87,7 +88,7 @@ def __init__( # type: ignore[no-untyped-def] self.description = description self.disable_execute_api_endpoint = disable_execute_api_endpoint - def _construct_http_api(self): # type: ignore[no-untyped-def] + def _construct_http_api(self) -> ApiGatewayV2HttpApi: """Constructs and returns the ApiGatewayV2 HttpApi. :returns: the HttpApi to which this SAM Api corresponds @@ -101,21 +102,21 @@ def _construct_http_api(self): # type: ignore[no-untyped-def] ) if self.cors_configuration: # call this method to add cors in open api - self._add_cors() # type: ignore[no-untyped-call] + self._add_cors() - self._add_auth() # type: ignore[no-untyped-call] - self._add_tags() # type: ignore[no-untyped-call] + self._add_auth() + self._add_tags() if self.fail_on_warnings: http_api.FailOnWarnings = self.fail_on_warnings if self.disable_execute_api_endpoint is not None: - self._add_endpoint_configuration() # type: ignore[no-untyped-call] + self._add_endpoint_configuration() - self._add_description() # type: ignore[no-untyped-call] + self._add_description() if self.definition_uri: - http_api.BodyS3Location = self._construct_body_s3_dict() # type: ignore[no-untyped-call] + http_api.BodyS3Location = self._construct_body_s3_dict(self.definition_uri) elif self.definition_body: http_api.Body = self.definition_body else: @@ -128,7 +129,7 @@ def _construct_http_api(self): # type: ignore[no-untyped-def] return http_api - def _add_endpoint_configuration(self): # type: ignore[no-untyped-def] + def _add_endpoint_configuration(self) -> None: """Add disableExecuteApiEndpoint if it is set in SAM HttpApi doesn't have vpcEndpointIds @@ -144,17 +145,17 @@ def _add_endpoint_configuration(self): # type: ignore[no-untyped-def] raise InvalidResourceException( self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." ) - editor = OpenApiEditor(self.definition_body) # type: ignore[no-untyped-call] + editor = OpenApiEditor(self.definition_body) # if DisableExecuteApiEndpoint is set in both definition_body and as a property, # SAM merges and overrides the disableExecuteApiEndpoint in definition_body with headers of # "x-amazon-apigateway-endpoint-configuration" - editor.add_endpoint_config(self.disable_execute_api_endpoint) # type: ignore[no-untyped-call] + editor.add_endpoint_config(self.disable_execute_api_endpoint) # Assign the OpenApi back to template self.definition_body = editor.openapi - def _add_cors(self): # type: ignore[no-untyped-def] + def _add_cors(self) -> None: """ Add CORS configuration if CORSConfiguration property is set in SAM. Adds CORS configuration only if DefinitionBody is present and @@ -202,7 +203,7 @@ def _add_cors(self): # type: ignore[no-untyped-def] "'AllowOrigin' is \"'*'\" or not set.", ) - editor = OpenApiEditor(self.definition_body) # type: ignore[no-untyped-call] + editor = OpenApiEditor(self.definition_body) # if CORS is set in both definition_body and as a CorsConfiguration property, # SAM merges and overrides the cors headers in definition_body with headers of CorsConfiguration editor.add_cors( # type: ignore[no-untyped-call] @@ -217,53 +218,61 @@ def _add_cors(self): # type: ignore[no-untyped-def] # Assign the OpenApi back to template self.definition_body = editor.openapi - def _construct_api_domain(self, http_api, route53_record_set_groups): # type: ignore[no-untyped-def] + def _construct_api_domain( + self, http_api: ApiGatewayV2HttpApi, route53_record_set_groups: Dict[str, Route53RecordSetGroup] + ) -> Tuple[ + Optional[ApiGatewayV2DomainName], + Optional[List[ApiGatewayV2ApiMapping]], + Optional[Route53RecordSetGroup], + ]: """ Constructs and returns the ApiGateway Domain and BasepathMapping """ if self.domain is None: return None, None, None - if self.domain.get("DomainName") is None or self.domain.get("CertificateArn") is None: + custom_domain_config = self.domain # not creating a copy as we will mutate it + domain_name = custom_domain_config.get("DomainName") + + domain_name_config = {} + + certificate_arn = custom_domain_config.get("CertificateArn") + if domain_name is None or certificate_arn is None: raise InvalidResourceException( self.logical_id, "Custom Domains only works if both DomainName and CertificateArn are provided." ) + domain_name_config["CertificateArn"] = certificate_arn - self.domain["ApiDomainName"] = "{}{}".format( - "ApiGatewayDomainNameV2", LogicalIdGenerator("", self.domain.get("DomainName")).gen() # type: ignore[no-untyped-call, no-untyped-call] - ) + api_domain_name = "{}{}".format("ApiGatewayDomainNameV2", LogicalIdGenerator("", domain_name).gen()) + custom_domain_config["ApiDomainName"] = api_domain_name - domain = ApiGatewayV2DomainName( - self.domain.get("ApiDomainName"), attributes=self.passthrough_resource_attributes - ) - domain_config = {} - domain.DomainName = self.domain.get("DomainName") + domain = ApiGatewayV2DomainName(api_domain_name, attributes=self.passthrough_resource_attributes) + domain.DomainName = domain_name domain.Tags = self.tags - endpoint = self.domain.get("EndpointConfiguration") - if endpoint is None: - endpoint = "REGIONAL" + endpoint_config = custom_domain_config.get("EndpointConfiguration") + if endpoint_config is None: + endpoint_config = "REGIONAL" # to make sure that default is always REGIONAL - self.domain["EndpointConfiguration"] = "REGIONAL" - elif endpoint not in ["REGIONAL"]: + custom_domain_config["EndpointConfiguration"] = "REGIONAL" + elif endpoint_config not in ["REGIONAL"]: raise InvalidResourceException( self.logical_id, "EndpointConfiguration for Custom Domains must be one of {}.".format(["REGIONAL"]), ) - domain_config["EndpointType"] = endpoint + domain_name_config["EndpointType"] = endpoint_config - if self.domain.get("OwnershipVerificationCertificateArn", None): - domain_config["OwnershipVerificationCertificateArn"] = self.domain.get( - "OwnershipVerificationCertificateArn" - ) + ownership_verification_certificate_arn = custom_domain_config.get("OwnershipVerificationCertificateArn") + if ownership_verification_certificate_arn: + domain_name_config["OwnershipVerificationCertificateArn"] = ownership_verification_certificate_arn - domain_config["CertificateArn"] = self.domain.get("CertificateArn") - if self.domain.get("SecurityPolicy", None): - domain_config["SecurityPolicy"] = self.domain.get("SecurityPolicy") + security_policy = custom_domain_config.get("SecurityPolicy") + if security_policy: + domain_name_config["SecurityPolicy"] = security_policy - domain.DomainNameConfigurations = [domain_config] + domain.DomainNameConfigurations = [domain_name_config] - mutual_tls_auth = self.domain.get("MutualTlsAuthentication", None) + mutual_tls_auth = custom_domain_config.get("MutualTlsAuthentication", None) if mutual_tls_auth: if isinstance(mutual_tls_auth, dict): if not set(mutual_tls_auth.keys()).issubset({"TruststoreUri", "TruststoreVersion"}): @@ -280,71 +289,90 @@ def _construct_api_domain(self, http_api, route53_record_set_groups): # type: i ) domain.MutualTlsAuthentication = {} if mutual_tls_auth.get("TruststoreUri", None): - domain.MutualTlsAuthentication["TruststoreUri"] = mutual_tls_auth["TruststoreUri"] # type: ignore[attr-defined] + domain.MutualTlsAuthentication["TruststoreUri"] = mutual_tls_auth["TruststoreUri"] if mutual_tls_auth.get("TruststoreVersion", None): - domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"] # type: ignore[attr-defined] + domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"] else: raise InvalidResourceException( - mutual_tls_auth, + self.logical_id, "MutualTlsAuthentication must be a map with at least one of the following fields {}.".format( ["TruststoreUri", "TruststoreVersion"] ), ) # Create BasepathMappings - if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), str): - basepaths = [self.domain.get("BasePath")] - elif self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), list): - basepaths = self.domain.get("BasePath") + basepaths: Optional[List[str]] + basepath_value = self.domain.get("BasePath") + if basepath_value and isinstance(basepath_value, str): + basepaths = [basepath_value] + elif basepath_value and isinstance(basepath_value, list): + basepaths = cast(Optional[List[str]], basepath_value) else: basepaths = None - basepath_resource_list = self._construct_basepath_mappings(basepaths, http_api) # type: ignore[no-untyped-call] + basepath_resource_list = self._construct_basepath_mappings(basepaths, http_api, api_domain_name) # Create the Route53 RecordSetGroup resource - record_set_group = self._construct_route53_recordsetgroup(route53_record_set_groups) # type: ignore[no-untyped-call] + record_set_group = self._construct_route53_recordsetgroup( + self.domain, route53_record_set_groups, api_domain_name + ) return domain, basepath_resource_list, record_set_group - def _construct_route53_recordsetgroup(self, route53_record_set_groups): # type: ignore[no-untyped-def] - if self.domain.get("Route53") is None: - return - route53 = self.domain.get("Route53") - if not isinstance(route53, dict): + def _construct_route53_recordsetgroup( + self, + custom_domain_config: Dict[str, Any], + route53_record_set_groups: Dict[str, Route53RecordSetGroup], + api_domain_name: str, + ) -> Optional[Route53RecordSetGroup]: + route53_config = custom_domain_config.get("Route53") + if route53_config is None: + return None + if not isinstance(route53_config, dict): raise InvalidResourceException( self.logical_id, "Invalid property type '{}' for Route53. " - "Expected a map defines an Amazon Route 53 configuration'.".format(type(route53).__name__), + "Expected a map defines an Amazon Route 53 configuration'.".format(type(route53_config).__name__), ) - if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None: + if route53_config.get("HostedZoneId") is None and route53_config.get("HostedZoneName") is None: raise InvalidResourceException( self.logical_id, "HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.", ) - logical_id_suffix = LogicalIdGenerator("", route53.get("HostedZoneId") or route53.get("HostedZoneName")).gen() # type: ignore[no-untyped-call, no-untyped-call] + logical_id_suffix = LogicalIdGenerator( + "", route53_config.get("HostedZoneId") or route53_config.get("HostedZoneName") + ).gen() logical_id = "RecordSetGroup" + logical_id_suffix - record_set_group = route53_record_set_groups.get(logical_id) - if not record_set_group: + matching_record_set_group = route53_record_set_groups.get(logical_id) + if matching_record_set_group: + record_set_group = matching_record_set_group + else: record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes) - if "HostedZoneId" in route53: - record_set_group.HostedZoneId = route53.get("HostedZoneId") - elif "HostedZoneName" in route53: - record_set_group.HostedZoneName = route53.get("HostedZoneName") + if "HostedZoneId" in route53_config: + record_set_group.HostedZoneId = route53_config.get("HostedZoneId") + elif "HostedZoneName" in route53_config: + record_set_group.HostedZoneName = route53_config.get("HostedZoneName") record_set_group.RecordSets = [] route53_record_set_groups[logical_id] = record_set_group - record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain) # type: ignore[no-untyped-call] + if record_set_group.RecordSets is None: + record_set_group.RecordSets = [] + record_set_group.RecordSets += self._construct_record_sets_for_domain( + custom_domain_config, route53_config, api_domain_name + ) return record_set_group - def _construct_basepath_mappings(self, basepaths, http_api): # type: ignore[no-untyped-def] - basepath_resource_list = [] + def _construct_basepath_mappings( + self, basepaths: Optional[List[str]], http_api: ApiGatewayV2HttpApi, api_domain_name: str + ) -> List[ApiGatewayV2ApiMapping]: + basepath_resource_list: List[ApiGatewayV2ApiMapping] = [] if basepaths is None: basepath_mapping = ApiGatewayV2ApiMapping( self.logical_id + "ApiMapping", attributes=self.passthrough_resource_attributes ) - basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName")) + basepath_mapping.DomainName = ref(api_domain_name) basepath_mapping.ApiId = ref(http_api.logical_id) basepath_mapping.Stage = ref(http_api.logical_id + ".Stage") basepath_resource_list.extend([basepath_mapping]) @@ -364,42 +392,46 @@ def _construct_basepath_mappings(self, basepaths, http_api): # type: ignore[no- logical_id = "{}{}{}".format(self.logical_id, re.sub(r"[\-_/]+", "", path), "ApiMapping") basepath_mapping = ApiGatewayV2ApiMapping(logical_id, attributes=self.passthrough_resource_attributes) - basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName")) + basepath_mapping.DomainName = ref(api_domain_name) basepath_mapping.ApiId = ref(http_api.logical_id) basepath_mapping.Stage = ref(http_api.logical_id + ".Stage") basepath_mapping.ApiMappingKey = path basepath_resource_list.extend([basepath_mapping]) return basepath_resource_list - def _construct_record_sets_for_domain(self, domain): # type: ignore[no-untyped-def] + def _construct_record_sets_for_domain( + self, custom_domain_config: Dict[str, Any], route53_config: Dict[str, Any], api_domain_name: str + ) -> List[Dict[str, Any]]: recordset_list = [] recordset = {} - route53 = domain.get("Route53") - recordset["Name"] = domain.get("DomainName") + recordset["Name"] = custom_domain_config.get("DomainName") recordset["Type"] = "A" - recordset["AliasTarget"] = self._construct_alias_target(self.domain) # type: ignore[no-untyped-call] + recordset["AliasTarget"] = self._construct_alias_target(custom_domain_config, route53_config, api_domain_name) recordset_list.extend([recordset]) recordset_ipv6 = {} - if route53.get("IpV6"): - recordset_ipv6["Name"] = domain.get("DomainName") + if route53_config.get("IpV6"): + recordset_ipv6["Name"] = custom_domain_config.get("DomainName") recordset_ipv6["Type"] = "AAAA" - recordset_ipv6["AliasTarget"] = self._construct_alias_target(self.domain) # type: ignore[no-untyped-call] + recordset_ipv6["AliasTarget"] = self._construct_alias_target( + custom_domain_config, route53_config, api_domain_name + ) recordset_list.extend([recordset_ipv6]) return recordset_list - def _construct_alias_target(self, domain): # type: ignore[no-untyped-def] + def _construct_alias_target( + self, domain_config: Dict[str, Any], route53_config: Dict[str, Any], api_domain_name: str + ) -> Dict[str, Any]: alias_target = {} - route53 = domain.get("Route53") - target_health = route53.get("EvaluateTargetHealth") + target_health = route53_config.get("EvaluateTargetHealth") if target_health is not None: alias_target["EvaluateTargetHealth"] = target_health - if domain.get("EndpointConfiguration") == "REGIONAL": - alias_target["HostedZoneId"] = fnGetAtt(self.domain.get("ApiDomainName"), "RegionalHostedZoneId") - alias_target["DNSName"] = fnGetAtt(self.domain.get("ApiDomainName"), "RegionalDomainName") + if domain_config.get("EndpointConfiguration") == "REGIONAL": + alias_target["HostedZoneId"] = fnGetAtt(api_domain_name, "RegionalHostedZoneId") + alias_target["DNSName"] = fnGetAtt(api_domain_name, "RegionalDomainName") else: raise InvalidResourceException( self.logical_id, @@ -407,7 +439,7 @@ def _construct_alias_target(self, domain): # type: ignore[no-untyped-def] ) return alias_target - def _add_auth(self): # type: ignore[no-untyped-def] + def _add_auth(self) -> None: """ Add Auth configuration to the OAS file, if necessary """ @@ -428,18 +460,18 @@ def _add_auth(self): # type: ignore[no-untyped-def] self.logical_id, "Unable to add Auth configuration because 'DefinitionBody' does not contain a valid OpenApi definition.", ) - open_api_editor = OpenApiEditor(self.definition_body) # type: ignore[no-untyped-call] + open_api_editor = OpenApiEditor(self.definition_body) auth_properties = AuthProperties(**self.auth) - authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer) # type: ignore[no-untyped-call] + authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer) # authorizers is guaranteed to return a value or raise an exception - open_api_editor.add_authorizers_security_definitions(authorizers) # type: ignore[no-untyped-call] - self._set_default_authorizer( # type: ignore[no-untyped-call] + open_api_editor.add_authorizers_security_definitions(authorizers) + self._set_default_authorizer( open_api_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.Authorizers ) self.definition_body = open_api_editor.openapi - def _add_tags(self): # type: ignore[no-untyped-def] + def _add_tags(self) -> None: """ Adds tags to the Http Api, including a default SAM tag. """ @@ -463,13 +495,19 @@ def _add_tags(self): # type: ignore[no-untyped-def] self.tags = {} self.tags[HttpApiTagName] = "SAM" - open_api_editor = OpenApiEditor(self.definition_body) # type: ignore[no-untyped-call] + open_api_editor = OpenApiEditor(self.definition_body) # authorizers is guaranteed to return a value or raise an exception - open_api_editor.add_tags(self.tags) # type: ignore[no-untyped-call] + open_api_editor.add_tags(self.tags) self.definition_body = open_api_editor.openapi - def _set_default_authorizer(self, open_api_editor, authorizers, default_authorizer, api_authorizers): # type: ignore[no-untyped-def] + def _set_default_authorizer( + self, + open_api_editor: OpenApiEditor, + authorizers: Dict[str, ApiGatewayV2Authorizer], + default_authorizer: str, + api_authorizers: Dict[str, Any], + ) -> None: """ Sets the default authorizer if one is given in the template :param open_api_editor: editor object that contains the OpenApi definition @@ -498,17 +536,17 @@ def _set_default_authorizer(self, open_api_editor, authorizers, default_authoriz ) for path in open_api_editor.iter_on_path(): - open_api_editor.set_path_default_authorizer( - path, default_authorizer, authorizers=authorizers, api_authorizers=api_authorizers - ) + open_api_editor.set_path_default_authorizer(path, default_authorizer, authorizers, api_authorizers) - def _get_authorizers(self, authorizers_config, enable_iam_authorizer=False): # type: ignore[no-untyped-def] + def _get_authorizers( + self, authorizers_config: Any, enable_iam_authorizer: bool = False + ) -> Dict[str, ApiGatewayV2Authorizer]: """ Returns all authorizers for an API as an ApiGatewayV2Authorizer object :param authorizers_config: authorizer configuration from the API Auth section :param enable_iam_authorizer: if True add an "AWS_IAM" authorizer """ - authorizers = {} + authorizers: Dict[str, ApiGatewayV2Authorizer] = {} if enable_iam_authorizer is True: authorizers["AWS_IAM"] = ApiGatewayV2Authorizer(is_aws_iam_authorizer=True) # type: ignore[no-untyped-call] @@ -546,36 +584,37 @@ def _get_authorizers(self, authorizers_config, enable_iam_authorizer=False): # ) return authorizers - def _construct_body_s3_dict(self): # type: ignore[no-untyped-def] + def _construct_body_s3_dict(self, definition_url: Union[str, Dict[str, Any]]) -> Dict[str, Any]: """ Constructs the HttpApi's `BodyS3Location property`, from the SAM Api's DefinitionUri property. :returns: a BodyS3Location dict, containing the S3 Bucket, Key, and Version of the OpenApi definition :rtype: dict """ - if isinstance(self.definition_uri, dict): - if not self.definition_uri.get("Bucket", None) or not self.definition_uri.get("Key", None): + if isinstance(definition_url, dict): + if not definition_url.get("Bucket", None) or not definition_url.get("Key", None): # DefinitionUri is a dictionary but does not contain Bucket or Key property raise InvalidResourceException( self.logical_id, "'DefinitionUri' requires Bucket and Key properties to be specified." ) - s3_pointer = self.definition_uri + s3_pointer = definition_url else: # DefinitionUri is a string - s3_pointer = parse_s3_uri(self.definition_uri) # type: ignore[no-untyped-call] - if s3_pointer is None: + _parsed_s3_pointer = parse_s3_uri(definition_url) + if _parsed_s3_pointer is None: raise InvalidResourceException( self.logical_id, "'DefinitionUri' is not a valid S3 Uri of the form " "'s3://bucket/key' with optional versionId query parameter.", ) + s3_pointer = _parsed_s3_pointer body_s3 = {"Bucket": s3_pointer["Bucket"], "Key": s3_pointer["Key"]} if "Version" in s3_pointer: body_s3["Version"] = s3_pointer["Version"] return body_s3 - def _construct_stage(self): # type: ignore[no-untyped-def] + def _construct_stage(self) -> Optional[ApiGatewayV2Stage]: """Constructs and returns the ApiGatewayV2 Stage. :returns: the Stage to which this SAM Api corresponds @@ -590,7 +629,7 @@ def _construct_stage(self): # type: ignore[no-untyped-def] and not self.default_route_settings and not self.route_settings ): - return + return None # If StageName is some intrinsic function, then don't prefix the Stage's logical ID # This will NOT create duplicates because we allow only ONE stage per API resource @@ -600,8 +639,8 @@ def _construct_stage(self): # type: ignore[no-untyped-def] elif stage_name_prefix == DefaultStageName: stage_logical_id = self.logical_id + "ApiGatewayDefaultStage" else: - generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) # type: ignore[no-untyped-call] - stage_logical_id = generator.gen() # type: ignore[no-untyped-call] + generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) + stage_logical_id = generator.gen() stage = ApiGatewayV2Stage(stage_logical_id, attributes=self.passthrough_resource_attributes) stage.ApiId = ref(self.logical_id) stage.StageName = self.stage_name @@ -614,7 +653,7 @@ def _construct_stage(self): # type: ignore[no-untyped-def] return stage - def _add_description(self): # type: ignore[no-untyped-def] + def _add_description(self) -> None: """Add description to DefinitionBody if Description property is set in SAM""" if not self.description: return @@ -631,19 +670,27 @@ def _add_description(self): # type: ignore[no-untyped-def] "'DefinitionBody' property.", ) - open_api_editor = OpenApiEditor(self.definition_body) # type: ignore[no-untyped-call] - open_api_editor.add_description(self.description) # type: ignore[no-untyped-call] + open_api_editor = OpenApiEditor(self.definition_body) + open_api_editor.add_description(self.description) self.definition_body = open_api_editor.openapi - @cw_timer(prefix="Generator", name="HttpApi") # type: ignore[no-untyped-call] - def to_cloudformation(self, route53_record_set_groups): # type: ignore[no-untyped-def] + @cw_timer(prefix="Generator", name="HttpApi") # type: ignore[misc] + def to_cloudformation( + self, route53_record_set_groups: Dict[str, Route53RecordSetGroup] + ) -> Tuple[ + ApiGatewayV2HttpApi, + Optional[ApiGatewayV2Stage], + Optional[ApiGatewayV2DomainName], + Optional[List[ApiGatewayV2ApiMapping]], + Optional[Route53RecordSetGroup], + ]: """Generates CloudFormation resources from a SAM HTTP API resource :returns: a tuple containing the HttpApi and Stage for an empty Api. :rtype: tuple """ - http_api = self._construct_http_api() # type: ignore[no-untyped-call] - domain, basepath_mapping, route53 = self._construct_api_domain(http_api, route53_record_set_groups) # type: ignore[no-untyped-call] - stage = self._construct_stage() # type: ignore[no-untyped-call] + http_api = self._construct_http_api() + domain, basepath_mapping, route53 = self._construct_api_domain(http_api, route53_record_set_groups) + stage = self._construct_stage() return http_api, stage, domain, basepath_mapping, route53 diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 65da89a77..52fbbe39e 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -109,8 +109,8 @@ def make_auto_deployable( # type: ignore[no-untyped-def] if function_names and function_names.get(self.logical_id[:-10], None): hash_input.append(function_names.get(self.logical_id[:-10], "")) data = self._X_HASH_DELIMITER.join(hash_input) - generator = logical_id_generator.LogicalIdGenerator(self.logical_id, data) # type: ignore[no-untyped-call] - self.logical_id = generator.gen() # type: ignore[no-untyped-call] + generator = logical_id_generator.LogicalIdGenerator(self.logical_id, data) + self.logical_id = generator.gen() digest = generator.get_hash(length=40) # type: ignore[no-untyped-call] # Get the full hash self.Description = "RestApi deployment id: {}".format(digest) stage.update_deployment_ref(self.logical_id) diff --git a/samtranslator/model/apigatewayv2.py b/samtranslator/model/apigatewayv2.py index 583156196..b8e6548ea 100644 --- a/samtranslator/model/apigatewayv2.py +++ b/samtranslator/model/apigatewayv2.py @@ -1,8 +1,11 @@ +from typing import Any, Dict, List, Optional + from samtranslator.model import PropertyType, Resource from samtranslator.model.types import is_type, one_of, is_str, list_of from samtranslator.model.intrinsics import ref, fnSub from samtranslator.model.exceptions import InvalidResourceException from samtranslator.translator.arn_generator import ArnGenerator +from samtranslator.utils.types import Intrinsicable APIGATEWAY_AUTHORIZER_KEY = "x-amazon-apigateway-authorizer" @@ -49,6 +52,11 @@ class ApiGatewayV2DomainName(Resource): "Tags": PropertyType(False, is_type(dict)), } + DomainName: Intrinsicable[str] + DomainNameConfigurations: Optional[List[Dict[str, Any]]] + MutualTlsAuthentication: Optional[Dict[str, Any]] + Tags: Optional[Dict[str, Any]] + class ApiGatewayV2ApiMapping(Resource): resource_type = "AWS::ApiGatewayV2::ApiMapping" @@ -96,7 +104,7 @@ def __init__( # type: ignore[no-untyped-def] # Validate necessary parameters exist if authorizer_type == "JWT": - self._validate_jwt_authorizer() # type: ignore[no-untyped-call] + self._validate_jwt_authorizer() if authorizer_type == "REQUEST": self._validate_lambda_authorizer() # type: ignore[no-untyped-call] @@ -152,7 +160,7 @@ def _validate_input_parameters(self): # type: ignore[no-untyped-def] self.api_logical_id, "EnableSimpleResponses must be defined only for Lambda Authorizer." ) - def _validate_jwt_authorizer(self): # type: ignore[no-untyped-def] + def _validate_jwt_authorizer(self) -> None: if not self.jwt_configuration: raise InvalidResourceException( self.api_logical_id, f"{self.name} OAuth2 Authorizer must define 'JwtConfiguration'." @@ -185,7 +193,7 @@ def _validate_lambda_authorizer(self): # type: ignore[no-untyped-def] self.name + " Lambda Authorizer property identity's 'Headers' is of invalid type.", ) - def generate_openapi(self): # type: ignore[no-untyped-def] + def generate_openapi(self) -> Dict[str, Any]: """ Generates OAS for the securitySchemes section """ diff --git a/samtranslator/model/eventsources/cloudwatchlogs.py b/samtranslator/model/eventsources/cloudwatchlogs.py index 9bd91e3ce..24cd4ec19 100644 --- a/samtranslator/model/eventsources/cloudwatchlogs.py +++ b/samtranslator/model/eventsources/cloudwatchlogs.py @@ -15,7 +15,7 @@ class CloudWatchLogs(PushEventSource): principal = "logs.amazonaws.com" property_types = {"LogGroupName": PropertyType(True, is_str()), "FilterPattern": PropertyType(True, is_str())} - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the CloudWatch Logs Subscription Filter and Lambda Permission to which this CloudWatch Logs event source corresponds. diff --git a/samtranslator/model/eventsources/pull.py b/samtranslator/model/eventsources/pull.py index 530287eb1..1b6ed2b33 100644 --- a/samtranslator/model/eventsources/pull.py +++ b/samtranslator/model/eventsources/pull.py @@ -58,7 +58,7 @@ def get_policy_arn(self): # type: ignore[no-untyped-def] def get_policy_statements(self): # type: ignore[no-untyped-def] raise NotImplementedError("Subclass must implement this method") - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the Lambda EventSourceMapping to which this pull event corresponds. Adds the appropriate managed policy to the function's execution role, if such a role is provided. diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 4e4d98954..cf00294d8 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -70,8 +70,8 @@ def _construct_permission( # type: ignore[no-untyped-def] if suffix.isalnum(): permission_logical_id = prefix + "Permission" + suffix else: - generator = logical_id_generator.LogicalIdGenerator(prefix + "Permission", suffix) # type: ignore[no-untyped-call] - permission_logical_id = generator.gen() # type: ignore[no-untyped-call] + generator = logical_id_generator.LogicalIdGenerator(prefix + "Permission", suffix) + permission_logical_id = generator.gen() lambda_permission = LambdaPermission( permission_logical_id, attributes=function.get_passthrough_resource_attributes() ) @@ -108,7 +108,7 @@ class Schedule(PushEventSource): "RetryPolicy": PropertyType(False, is_type(dict)), } - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the EventBridge Rule and Lambda Permission to which this Schedule event source corresponds. @@ -193,7 +193,7 @@ class CloudWatchEvent(PushEventSource): "State": PropertyType(False, is_str()), } - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the CloudWatch Events/EventBridge Rule and Lambda Permission to which this CloudWatch Events/EventBridge event source corresponds. @@ -289,7 +289,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] return {"bucket": resources[bucket_id], "bucket_id": bucket_id} raise InvalidEventException(self.relative_id, "S3 events must reference an S3 bucket in the same template.") - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the Lambda Permission resource allowing S3 to invoke the function this event source triggers. @@ -393,7 +393,7 @@ def _depend_on_lambda_permissions_using_tag(self, bucket, permission): # type: "Fn::If": [permission.resource_attributes[CONDITION], ref(permission.logical_id), "no dependency"] } } - properties["Tags"] = tags + get_tag_list(dep_tag) # type: ignore[no-untyped-call] + properties["Tags"] = tags + get_tag_list(dep_tag) return bucket def _inject_notification_configuration(self, function, bucket, bucket_id): # type: ignore[no-untyped-def] @@ -455,7 +455,7 @@ class SNS(PushEventSource): "RedrivePolicy": PropertyType(False, is_type(dict)), } - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """Returns the Lambda Permission resource allowing SNS to invoke the function this event source triggers. @@ -633,7 +633,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] return {"explicit_api": explicit_api, "explicit_api_stage": {"suffix": stage_suffix}} - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """If the Api event source has a RestApi property, then simply return the Lambda Permission resource allowing API Gateway to call the function. If no RestApi is provided, then additionally inject the path, method, and the @@ -951,7 +951,7 @@ class AlexaSkill(PushEventSource): property_types = {"SkillId": PropertyType(False, is_str())} - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] function = kwargs.get("function") @@ -970,7 +970,7 @@ class IoTRule(PushEventSource): property_types = {"Sql": PropertyType(True, is_str()), "AwsIotSqlVersion": PropertyType(False, is_str())} - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] function = kwargs.get("function") @@ -1033,7 +1033,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] self.relative_id, "Cognito events must reference a Cognito UserPool in the same template." ) - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] function = kwargs.get("function") @@ -1119,7 +1119,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] return {"explicit_api": explicit_api} - @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] """If the Api event source has a RestApi property, then simply return the Lambda Permission resource allowing API Gateway to call the function. If no RestApi is provided, then additionally inject the path, method, and the @@ -1165,7 +1165,7 @@ def _get_permission(self, resources_to_link, stage): # type: ignore[no-untyped- editor = None if resources_to_link["explicit_api"].get("DefinitionBody"): try: - editor = OpenApiEditor(resources_to_link["explicit_api"].get("DefinitionBody")) # type: ignore[no-untyped-call] + editor = OpenApiEditor(resources_to_link["explicit_api"].get("DefinitionBody")) except InvalidDocumentException as e: api_logical_id = self.ApiId.get("Ref") if isinstance(self.ApiId, dict) else self.ApiId # type: ignore[attr-defined] raise InvalidResourceException(api_logical_id, " ".join(ex.message for ex in e.causes)) @@ -1218,7 +1218,7 @@ def _add_openapi_integration(self, api, function, manage_swagger=False): # type uri = _build_apigw_integration_uri(function, "${AWS::Partition}") # type: ignore[no-untyped-call] - editor = OpenApiEditor(open_api_body) # type: ignore[no-untyped-call] + editor = OpenApiEditor(open_api_body) if manage_swagger and editor.has_integration(self.Path, self.Method): # type: ignore[attr-defined, no-untyped-call] # Cannot add the Lambda Integration, if it is already present diff --git a/samtranslator/model/eventsources/scheduler.py b/samtranslator/model/eventsources/scheduler.py index d99463d2f..c7fd92dfc 100644 --- a/samtranslator/model/eventsources/scheduler.py +++ b/samtranslator/model/eventsources/scheduler.py @@ -171,7 +171,7 @@ def _construct_execution_role( else: raise RuntimeError(f"Unexpected target type {target_type.name}") - role_logical_id = LogicalIdGenerator(self.logical_id + "Role").gen() # type: ignore[no-untyped-call, no-untyped-call] + role_logical_id = LogicalIdGenerator(self.logical_id + "Role").gen() execution_role = IAMRole(role_logical_id, attributes=passthrough_resource_attributes) execution_role.AssumeRolePolicyDocument = IAMRolePolicies.scheduler_assume_role_policy() diff --git a/samtranslator/model/route53.py b/samtranslator/model/route53.py index 3eb7ec4b7..1c4adadfa 100644 --- a/samtranslator/model/route53.py +++ b/samtranslator/model/route53.py @@ -1,5 +1,8 @@ +from typing import Any, List, Optional + from samtranslator.model import PropertyType, Resource from samtranslator.model.types import is_type, is_str +from samtranslator.utils.types import Intrinsicable class Route53RecordSetGroup(Resource): @@ -9,3 +12,7 @@ class Route53RecordSetGroup(Resource): "HostedZoneName": PropertyType(False, is_str()), "RecordSets": PropertyType(False, is_type(list)), } + + HostedZoneId: Optional[Intrinsicable[str]] + HostedZoneName: Optional[Intrinsicable[str]] + RecordSets: Optional[List[Any]] diff --git a/samtranslator/model/s3_utils/uri_parser.py b/samtranslator/model/s3_utils/uri_parser.py index b48fad446..df91b0137 100644 --- a/samtranslator/model/s3_utils/uri_parser.py +++ b/samtranslator/model/s3_utils/uri_parser.py @@ -1,8 +1,9 @@ +from typing import Any, Dict, Optional from urllib.parse import urlparse, parse_qs from samtranslator.model.exceptions import InvalidResourceException -def parse_s3_uri(uri): # type: ignore[no-untyped-def] +def parse_s3_uri(uri: Any) -> Optional[Dict[str, Any]]: """Parses a S3 Uri into a dictionary of the Bucket, Key, and VersionId :return: a BodyS3Location dict or None if not an S3 Uri @@ -82,15 +83,16 @@ def construct_s3_location_object(location_uri, logical_id, property_name): # ty else: # location_uri is NOT a dictionary. Parse it as a string - s3_pointer = parse_s3_uri(location_uri) # type: ignore[no-untyped-call] + _s3_pointer = parse_s3_uri(location_uri) - if s3_pointer is None: + if _s3_pointer is None: raise InvalidResourceException( logical_id, "'{}' is not a valid S3 Uri of the form " "'s3://bucket/key' with optional versionId query " "parameter.".format(property_name), ) + s3_pointer = _s3_pointer code = {"S3Bucket": s3_pointer["Bucket"], "S3Key": s3_pointer["Key"]} if "Version" in s3_pointer: diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index b71e58496..18a4efde1 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -398,7 +398,7 @@ def _get_or_make_condition(self, destination, logical_id, conditions): # type: def _make_gen_condition_name(self, name: str, hash_input: str) -> str: # Make sure the property name is not over 255 characters (CFN limit) - hash_digest = logical_id_generator.LogicalIdGenerator("", hash_input).gen() # type: ignore[no-untyped-call, no-untyped-call] + hash_digest = logical_id_generator.LogicalIdGenerator("", hash_input).gen() condition_name: str = name + hash_digest if len(condition_name) > 255: return input(condition_name)[:255] @@ -810,7 +810,7 @@ def _construct_version(self, function, intrinsics_resolver, code_sha256=None): logical_dict.update(function.Environment) if function.MemorySize: logical_dict.update({"MemorySize": function.MemorySize}) - logical_id = logical_id_generator.LogicalIdGenerator(prefix, logical_dict, code_sha256).gen() # type: ignore[no-untyped-call, no-untyped-call] + logical_id = logical_id_generator.LogicalIdGenerator(prefix, logical_dict, code_sha256).gen() attributes = self.get_passthrough_resource_attributes() # type: ignore[no-untyped-call] if attributes is None: @@ -1230,7 +1230,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] self.CorsConfiguration = intrinsics_resolver.resolve_parameter_refs(self.CorsConfiguration) # type: ignore[has-type] self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain) # type: ignore[has-type] - api_generator = HttpApiGenerator( # type: ignore[no-untyped-call] + api_generator = HttpApiGenerator( self.logical_id, self.StageVariables, # type: ignore[attr-defined] self.depends_on, @@ -1324,7 +1324,7 @@ def _construct_dynamodb_table(self): # type: ignore[no-untyped-def] dynamodb_table.TableName = self.TableName # type: ignore[attr-defined] if bool(self.Tags): # type: ignore[attr-defined] - dynamodb_table.Tags = get_tag_list(self.Tags) # type: ignore[attr-defined, no-untyped-call] + dynamodb_table.Tags = get_tag_list(self.Tags) # type: ignore[attr-defined] return dynamodb_table @@ -1457,7 +1457,7 @@ def _construct_lambda_layer(self, intrinsics_resolver): # type: ignore[no-untyp if "Metadata" in hash_dict.get(old_logical_id): del hash_dict[old_logical_id]["Metadata"] - new_logical_id = logical_id_generator.LogicalIdGenerator(old_logical_id, hash_dict).gen() # type: ignore[no-untyped-call, no-untyped-call] + new_logical_id = logical_id_generator.LogicalIdGenerator(old_logical_id, hash_dict).gen() self.logical_id = new_logical_id lambda_layer = LambdaLayerVersion(self.logical_id, depends_on=self.depends_on, attributes=attributes) @@ -1632,8 +1632,8 @@ class SamConnector(SamResourceMacro): "Permissions": PropertyType(True, list_of(is_str())), } - @cw_timer # type: ignore[misc] - def to_cloudformation(self, **kwargs) -> List: # type: ignore[no-untyped-def, type-arg] + @cw_timer + def to_cloudformation(self, **kwargs: Any) -> List[Resource]: # type: ignore resource_resolver: ResourceResolver = kwargs["resource_resolver"] original_template = kwargs["original_template"] diff --git a/samtranslator/model/stepfunctions/events.py b/samtranslator/model/stepfunctions/events.py index ca15c5403..2eee1fca9 100644 --- a/samtranslator/model/stepfunctions/events.py +++ b/samtranslator/model/stepfunctions/events.py @@ -42,8 +42,8 @@ def _generate_logical_id(self, prefix, suffix, resource_type): # type: ignore[n if suffix.isalnum(): logical_id = prefix + resource_type + suffix else: - generator = logical_id_generator.LogicalIdGenerator(prefix + resource_type, suffix) # type: ignore[no-untyped-call] - logical_id = generator.gen() # type: ignore[no-untyped-call] + generator = logical_id_generator.LogicalIdGenerator(prefix + resource_type, suffix) + logical_id = generator.gen() return logical_id def _construct_role(self, resource, permissions_boundary=None, prefix=None, suffix=""): # type: ignore[no-untyped-def] @@ -90,7 +90,7 @@ class Schedule(EventSource): "RetryPolicy": PropertyType(False, is_type(dict)), } - @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def] """Returns the EventBridge Rule and IAM Role to which this Schedule event source corresponds. @@ -174,7 +174,7 @@ class CloudWatchEvent(EventSource): "State": PropertyType(False, is_str()), } - @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def] """Returns the CloudWatch Events/EventBridge Rule and IAM Role to which this CloudWatch Events/EventBridge event source corresponds. @@ -302,7 +302,7 @@ def resources_to_link(self, resources): # type: ignore[no-untyped-def] return {"explicit_api": explicit_api, "explicit_api_stage": {"suffix": stage_suffix}} - @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX) def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def] """If the Api event source has a RestApi property, then simply return the IAM role resource allowing API Gateway to start the state machine execution. If no RestApi is provided, then diff --git a/samtranslator/model/stepfunctions/generators.py b/samtranslator/model/stepfunctions/generators.py index 1a0865f35..ee3faef3b 100644 --- a/samtranslator/model/stepfunctions/generators.py +++ b/samtranslator/model/stepfunctions/generators.py @@ -94,7 +94,7 @@ def __init__( # type: ignore[no-untyped-def] ) self.substitution_counter = 1 - @cw_timer(prefix="Generator", name="StateMachine") # type: ignore[no-untyped-call] + @cw_timer(prefix="Generator", name="StateMachine") def to_cloudformation(self): # type: ignore[no-untyped-def] """ Constructs and returns the State Machine resource and any additional resources associated with it. @@ -171,13 +171,14 @@ def _construct_definition_uri(self): # type: ignore[no-untyped-def] s3_pointer = self.definition_uri else: # DefinitionUri is a string - s3_pointer = parse_s3_uri(self.definition_uri) # type: ignore[no-untyped-call] - if s3_pointer is None: + parsed_s3_pointer = parse_s3_uri(self.definition_uri) + if parsed_s3_pointer is None: raise InvalidResourceException( self.logical_id, "'DefinitionUri' is not a valid S3 Uri of the form " "'s3://bucket/key' with optional versionId query parameter.", ) + s3_pointer = parsed_s3_pointer definition_s3 = {"Bucket": s3_pointer["Bucket"], "Key": s3_pointer["Key"]} if "Version" in s3_pointer: @@ -236,7 +237,7 @@ def _construct_tag_list(self): # type: ignore[no-untyped-def] :rtype: list """ sam_tag = {self._SAM_KEY: self._SAM_VALUE} - return get_tag_list(sam_tag) + get_tag_list(self.tags) # type: ignore[no-untyped-call] + return get_tag_list(sam_tag) + get_tag_list(self.tags) def _generate_event_resources(self): # type: ignore[no-untyped-def] """Generates and returns the resources associated with this state machine's event sources. diff --git a/samtranslator/model/tags/resource_tagging.py b/samtranslator/model/tags/resource_tagging.py index a2f0125a5..614ca2d97 100644 --- a/samtranslator/model/tags/resource_tagging.py +++ b/samtranslator/model/tags/resource_tagging.py @@ -1,9 +1,11 @@ # Constants for Tagging +from typing import Any, Dict, List, Optional + _KEY = "Key" _VALUE = "Value" -def get_tag_list(resource_tag_dict): # type: ignore[no-untyped-def] +def get_tag_list(resource_tag_dict: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Transforms the SAM defined Tags into the form CloudFormation is expecting. diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 40e373ca3..96c5ae340 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -1,10 +1,12 @@ import copy import re -from typing import Any, Iterator, Optional +from typing import Any, Dict, Iterator, List, Optional +from samtranslator.model.apigatewayv2 import ApiGatewayV2Authorizer from samtranslator.model.intrinsics import ref, make_conditional, is_intrinsic, is_intrinsic_no_value from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr +from samtranslator.utils.types import Intrinsicable import json @@ -32,7 +34,7 @@ class OpenApiEditor(object): _ALL_HTTP_METHODS = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"] _DEFAULT_PATH = "$default" - def __init__(self, doc): # type: ignore[no-untyped-def] + def __init__(self, doc: Optional[Dict[str, Any]]) -> None: """ Initialize the class with a swagger dictionary. This class creates a copy of the Swagger and performs all modifications on this copy. @@ -40,7 +42,7 @@ def __init__(self, doc): # type: ignore[no-untyped-def] :param dict doc: OpenApi document as a dictionary :raises InvalidDocumentException: If the input OpenApi document does not meet the basic OpenApi requirements. """ - if not OpenApiEditor.is_valid(doc): + if not doc or not OpenApiEditor.is_valid(doc): raise InvalidDocumentException( [ InvalidTemplateException( @@ -352,7 +354,7 @@ def add_payload_format_version_to_method(self, api, path, method_name, payload_f for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): # type: ignore[no-untyped-call] method_definition[self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = payload_format_version - def add_authorizers_security_definitions(self, authorizers): # type: ignore[no-untyped-def] + def add_authorizers_security_definitions(self, authorizers: Dict[str, ApiGatewayV2Authorizer]) -> None: """ Add Authorizer definitions to the securityDefinitions part of Swagger. @@ -363,7 +365,13 @@ def add_authorizers_security_definitions(self, authorizers): # type: ignore[no- for authorizer_name, authorizer in authorizers.items(): self.security_schemes[authorizer_name] = authorizer.generate_openapi() - def set_path_default_authorizer(self, path, default_authorizer, authorizers, api_authorizers): # type: ignore[no-untyped-def] + def set_path_default_authorizer( + self, + path: str, + default_authorizer: str, + authorizers: Dict[str, ApiGatewayV2Authorizer], + api_authorizers: Dict[str, Any], + ) -> None: """ Adds the default_authorizer to the security block for each method on this path unless an Authorizer was defined at the Function/Path/Method level. This is intended to be used to set the @@ -373,7 +381,7 @@ def set_path_default_authorizer(self, path, default_authorizer, authorizers, api :param string path: Path name :param string default_authorizer: Name of the authorizer to use as the default. Must be a key in the authorizers param. - :param list authorizers: List of Authorizer configurations defined on the related Api. + :param dict authorizers: Dict of Authorizer configurations defined on the related Api. """ for path_item in self.get_conditional_contents(self.paths.get(path)): # type: ignore[no-untyped-call] for method_name, method in path_item.items(): @@ -408,7 +416,7 @@ def set_path_default_authorizer(self, path, default_authorizer, authorizers, api existing_security = method_definition.get("security", []) if existing_security: continue - authorizer_list = [] + authorizer_list: List[str] = [] if authorizers: authorizer_list.extend(authorizers.keys()) security_dict = {} @@ -475,7 +483,7 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers if security: method_definition["security"] = security - def add_tags(self, tags): # type: ignore[no-untyped-def] + def add_tags(self, tags: Dict[str, Intrinsicable[str]]) -> None: """ Adds tags to the OpenApi definition using an ApiGateway extension for tag values. @@ -503,7 +511,7 @@ def add_tags(self, tags): # type: ignore[no-untyped-def] tag[self._X_APIGW_TAG_VALUE] = value self.tags.append(tag) - def add_endpoint_config(self, disable_execute_api_endpoint): # type: ignore[no-untyped-def] + def add_endpoint_config(self, disable_execute_api_endpoint: Optional[Intrinsicable[bool]]) -> None: """Add endpoint configuration to _X_APIGW_ENDPOINT_CONFIG header in open api definition Following this guide: @@ -587,7 +595,7 @@ def add_cors( # type: ignore[no-untyped-def] self._doc[self._X_APIGW_CORS] = cors_configuration - def add_description(self, description): # type: ignore[no-untyped-def] + def add_description(self, description: Intrinsicable[str]) -> None: """Add description in open api definition, if it is not already defined :param string description: Description of the API diff --git a/samtranslator/plugins/api/default_definition_body_plugin.py b/samtranslator/plugins/api/default_definition_body_plugin.py index 2d5e37414..219aabcb4 100644 --- a/samtranslator/plugins/api/default_definition_body_plugin.py +++ b/samtranslator/plugins/api/default_definition_body_plugin.py @@ -21,7 +21,7 @@ def __init__(self) -> None: super(DefaultDefinitionBodyPlugin, self).__init__(DefaultDefinitionBodyPlugin.__name__) - @cw_timer(prefix="Plugin-DefaultDefinitionBody") # type: ignore[no-untyped-call] + @cw_timer(prefix="Plugin-DefaultDefinitionBody") def on_before_transform_template(self, template_dict): # type: ignore[no-untyped-def] """ Hook method that gets called before the SAM template is processed. diff --git a/samtranslator/plugins/api/implicit_api_plugin.py b/samtranslator/plugins/api/implicit_api_plugin.py index 6d2215157..1bc012f8d 100644 --- a/samtranslator/plugins/api/implicit_api_plugin.py +++ b/samtranslator/plugins/api/implicit_api_plugin.py @@ -62,7 +62,7 @@ def _setup_api_properties(self) -> None: "Method _setup_api_properties() must be implemented in a subclass of ImplicitApiPlugin" ) - @cw_timer(prefix="Plugin-ImplicitApi") # type: ignore[no-untyped-call] + @cw_timer(prefix="Plugin-ImplicitApi") def on_before_transform_template(self, template_dict): # type: ignore[no-untyped-def] """ Hook method that gets called before the SAM template is processed. diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index ccbf8c1a3..7b74d7c1c 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -70,7 +70,7 @@ def __init__(self, sar_client=None, wait_for_template_active_status=False, valid message = "Cannot set both validate_only and wait_for_template_active_status flags to True." raise InvalidPluginException(ServerlessAppPlugin.__name__, message) # type: ignore[no-untyped-call] - @cw_timer(prefix=PLUGIN_METRICS_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=PLUGIN_METRICS_PREFIX) def on_before_transform_template(self, template_dict): # type: ignore[no-untyped-def] """ Hook method that gets called before the SAM template is processed. @@ -230,7 +230,7 @@ def _sanitize_sar_str_param(self, param): # type: ignore[no-untyped-def] return None return str(param) - @cw_timer(prefix=PLUGIN_METRICS_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=PLUGIN_METRICS_PREFIX) def on_before_transform_resource(self, logical_id, resource_type, resource_properties): # type: ignore[no-untyped-def] """ Hook method that gets called before "each" SAM resource gets processed @@ -306,7 +306,7 @@ def _check_for_dictionary_key(self, logical_id, dictionary, keys): # type: igno if key not in dictionary: raise InvalidResourceException(logical_id, f"Resource is missing the required [{key}] property.") - @cw_timer(prefix=PLUGIN_METRICS_PREFIX) # type: ignore[no-untyped-call] + @cw_timer(prefix=PLUGIN_METRICS_PREFIX) def on_after_transform_template(self, template): # type: ignore[no-untyped-def] """ Hook method that gets called after the template is processed @@ -380,7 +380,7 @@ def _is_template_active(self, response, application_id, template_id): # type: i return status == "ACTIVE" - @cw_timer(prefix="External", name="SAR") # type: ignore[no-untyped-call] + @cw_timer(prefix="External", name="SAR") def _sar_service_call(self, service_call_lambda, logical_id, *args): # type: ignore[no-untyped-def] """ Handles service calls and exception management for service calls diff --git a/samtranslator/plugins/globals/globals_plugin.py b/samtranslator/plugins/globals/globals_plugin.py index aa98d0384..48e05de3a 100644 --- a/samtranslator/plugins/globals/globals_plugin.py +++ b/samtranslator/plugins/globals/globals_plugin.py @@ -19,7 +19,7 @@ def __init__(self) -> None: """ super(GlobalsPlugin, self).__init__(GlobalsPlugin.__name__) - @cw_timer(prefix="Plugin-Globals") # type: ignore[no-untyped-call] + @cw_timer(prefix="Plugin-Globals") def on_before_transform_template(self, template_dict): # type: ignore[no-untyped-def] """ Hook method that runs before a template gets transformed. In this method, we parse and process Globals section diff --git a/samtranslator/plugins/policies/policy_templates_plugin.py b/samtranslator/plugins/policies/policy_templates_plugin.py index eb579e0ea..133b762e7 100644 --- a/samtranslator/plugins/policies/policy_templates_plugin.py +++ b/samtranslator/plugins/policies/policy_templates_plugin.py @@ -31,7 +31,7 @@ def __init__(self, policy_template_processor): # type: ignore[no-untyped-def] self._policy_template_processor = policy_template_processor - @cw_timer(prefix="Plugin-PolicyTemplates") # type: ignore[no-untyped-call] + @cw_timer(prefix="Plugin-PolicyTemplates") def on_before_transform_resource(self, logical_id, resource_type, resource_properties): # type: ignore[no-untyped-def] """ Hook method that gets called before "each" SAM resource gets processed diff --git a/samtranslator/translator/logical_id_generator.py b/samtranslator/translator/logical_id_generator.py index bd5e471c8..ca44ad2e4 100644 --- a/samtranslator/translator/logical_id_generator.py +++ b/samtranslator/translator/logical_id_generator.py @@ -1,6 +1,7 @@ import hashlib import json import sys +from typing import Any, Optional class LogicalIdGenerator(object): @@ -9,7 +10,7 @@ class LogicalIdGenerator(object): # given by this class HASH_LENGTH = 10 - def __init__(self, prefix, data_obj=None, data_hash=None): # type: ignore[no-untyped-def] + def __init__(self, prefix: str, data_obj: Optional[Any] = None, data_hash: Optional[str] = None) -> None: """ Generate logical IDs for resources that are stable, deterministic and platform independent @@ -26,7 +27,7 @@ def __init__(self, prefix, data_obj=None, data_hash=None): # type: ignore[no-un self.data_str = data_str self.data_hash = data_hash - def gen(self): # type: ignore[no-untyped-def] + def gen(self) -> str: """ Generate stable LogicalIds based on the prefix and given data. This method ensures that the logicalId is deterministic and stable based on input prefix & data object. In other words: diff --git a/samtranslator/translator/managed_policy_translator.py b/samtranslator/translator/managed_policy_translator.py index 035b6b60d..6f1f0b16c 100644 --- a/samtranslator/translator/managed_policy_translator.py +++ b/samtranslator/translator/managed_policy_translator.py @@ -11,7 +11,7 @@ def __init__(self, iam_client): # type: ignore[no-untyped-def] self._policy_map = None self.max_items = 1000 - @cw_timer(prefix="External", name="IAM") # type: ignore[no-untyped-call] + @cw_timer(prefix="External", name="IAM") def _load_policies_from_iam(self): # type: ignore[no-untyped-def] LOG.info("Loading policies from IAM...") diff --git a/samtranslator/utils/types.py b/samtranslator/utils/types.py new file mode 100644 index 000000000..5fd723d6f --- /dev/null +++ b/samtranslator/utils/types.py @@ -0,0 +1,6 @@ +"""Type related utils.""" +from typing import Any, Dict, TypeVar, Union + +T = TypeVar("T") + +Intrinsicable = Union[Dict[str, Any], T] diff --git a/tests/translator/output/error_httpapi_mtls_configuration_invalid_type.json b/tests/translator/output/error_httpapi_mtls_configuration_invalid_type.json index 6b2026623..2990f5cdd 100644 --- a/tests/translator/output/error_httpapi_mtls_configuration_invalid_type.json +++ b/tests/translator/output/error_httpapi_mtls_configuration_invalid_type.json @@ -1,8 +1,8 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [['TruststoreUri', 'TruststoreVersion']] is invalid. MutualTlsAuthentication must be a map with at least one of the following fields ['TruststoreUri', 'TruststoreVersion'].", + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApi] is invalid. MutualTlsAuthentication must be a map with at least one of the following fields ['TruststoreUri', 'TruststoreVersion'].", "errors": [ { - "errorMessage": "Resource with id [['TruststoreUri', 'TruststoreVersion']] is invalid. MutualTlsAuthentication must contains one of the following fields ['TruststoreUri', 'TruststoreVersion']." + "errorMessage": "Resource with id [MyApi] is invalid. MutualTlsAuthentication must contains one of the following fields ['TruststoreUri', 'TruststoreVersion']." } ] } From 68812d09323b913b72830b618b4944708253febb Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 29 Nov 2022 03:11:44 +0000 Subject: [PATCH 3/4] feat: Add SnapStart support --- samtranslator/model/lambda_.py | 1 + samtranslator/model/sam_resources.py | 5 + samtranslator/plugins/globals/globals.py | 5 +- .../input/function_with_snapstart.yaml | 35 +++ .../input/globals_for_function.yaml | 4 + .../aws-cn/function_with_snapstart.json | 204 ++++++++++++++++++ .../output/aws-cn/globals_for_function.json | 10 +- .../aws-us-gov/function_with_snapstart.json | 204 ++++++++++++++++++ .../aws-us-gov/globals_for_function.json | 10 +- .../output/function_with_snapstart.json | 204 ++++++++++++++++++ .../output/globals_for_function.json | 10 +- tests/translator/test_function_resources.py | 22 ++ 12 files changed, 707 insertions(+), 7 deletions(-) create mode 100644 tests/translator/input/function_with_snapstart.yaml create mode 100644 tests/translator/output/aws-cn/function_with_snapstart.json create mode 100644 tests/translator/output/aws-us-gov/function_with_snapstart.json create mode 100644 tests/translator/output/function_with_snapstart.json diff --git a/samtranslator/model/lambda_.py b/samtranslator/model/lambda_.py index 4a941620b..763148bfe 100644 --- a/samtranslator/model/lambda_.py +++ b/samtranslator/model/lambda_.py @@ -27,6 +27,7 @@ class LambdaFunction(Resource): "CodeSigningConfigArn": PropertyType(False, is_str()), "ImageConfig": PropertyType(False, is_type(dict)), "Architectures": PropertyType(False, list_of(one_of(is_str(), is_type(dict)))), + "SnapStart": PropertyType(False, is_type(dict)), "EphemeralStorage": PropertyType(False, is_type(dict)), } diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 18a4efde1..be76c90dc 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -112,6 +112,7 @@ class SamFunction(SamResourceMacro): "ImageConfig": PropertyType(False, is_type(dict)), "CodeSigningConfigArn": PropertyType(False, is_str()), "Architectures": PropertyType(False, list_of(one_of(is_str(), is_type(dict)))), + "SnapStart": PropertyType(False, is_type(dict)), "FunctionUrlConfig": PropertyType(False, is_type(dict)), } event_resolver = ResourceTypeResolver( # type: ignore[no-untyped-call] @@ -458,6 +459,7 @@ def _construct_lambda_function(self): # type: ignore[no-untyped-def] lambda_function.ImageConfig = self.ImageConfig # type: ignore[attr-defined] lambda_function.PackageType = self.PackageType # type: ignore[attr-defined] lambda_function.Architectures = self.Architectures # type: ignore[attr-defined] + lambda_function.SnapStart = self.SnapStart # type: ignore[attr-defined] lambda_function.EphemeralStorage = self.EphemeralStorage # type: ignore[attr-defined] if self.Tracing: # type: ignore[attr-defined] @@ -810,6 +812,9 @@ def _construct_version(self, function, intrinsics_resolver, code_sha256=None): logical_dict.update(function.Environment) if function.MemorySize: logical_dict.update({"MemorySize": function.MemorySize}) + # If SnapStart is enabled we want to publish a new version, to have the corresponding snapshot + if function.SnapStart and function.SnapStart.get("ApplyOn", "None") != "None": + logical_dict.update({"SnapStart": function.SnapStart}) logical_id = logical_id_generator.LogicalIdGenerator(prefix, logical_dict, code_sha256).gen() attributes = self.get_passthrough_resource_attributes() # type: ignore[no-untyped-call] diff --git a/samtranslator/plugins/globals/globals.py b/samtranslator/plugins/globals/globals.py index 4a79b8af0..9e687377f 100644 --- a/samtranslator/plugins/globals/globals.py +++ b/samtranslator/plugins/globals/globals.py @@ -45,6 +45,7 @@ class Globals(object): "FileSystemConfigs", "CodeSigningConfigArn", "Architectures", + "SnapStart", "EphemeralStorage", "FunctionUrlConfig", ], @@ -85,7 +86,9 @@ class Globals(object): SamResourceType.SimpleTable.value: ["SSESpecification"], } # unreleased_properties *must be* part of supported_properties too - unreleased_properties: Dict[str, List[str]] = {} + unreleased_properties: Dict[str, List[str]] = { + SamResourceType.Function.value: ["SnapStart"], + } def __init__(self, template): # type: ignore[no-untyped-def] """ diff --git a/tests/translator/input/function_with_snapstart.yaml b/tests/translator/input/function_with_snapstart.yaml new file mode 100644 index 000000000..4b3e08836 --- /dev/null +++ b/tests/translator/input/function_with_snapstart.yaml @@ -0,0 +1,35 @@ +%YAML 1.1 +--- +Parameters: + SnapStartParam: + Type: String + Default: None + +Resources: + SnapStartFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + SnapStart: + ApplyOn: PublishedVersions + + SnapStartParameterFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + SnapStart: + ApplyOn: !Ref SnapStartParam + + SnapStartFunctionWithAlias: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: live + SnapStart: + ApplyOn: PublishedVersions diff --git a/tests/translator/input/globals_for_function.yaml b/tests/translator/input/globals_for_function.yaml index 8f5702ce4..e9adecd42 100644 --- a/tests/translator/input/globals_for_function.yaml +++ b/tests/translator/input/globals_for_function.yaml @@ -26,6 +26,8 @@ Globals: ReservedConcurrentExecutions: 50 Architectures: - x86_64 + SnapStart: + ApplyOn: PublishedVersions EphemeralStorage: Size: 1024 @@ -54,4 +56,6 @@ Resources: PermissionsBoundary: arn:aws:1234:iam:boundary/OverridePermissionsBoundary Layers: - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:MyLayer2:2 + SnapStart: + ApplyOn: None ReservedConcurrentExecutions: 100 diff --git a/tests/translator/output/aws-cn/function_with_snapstart.json b/tests/translator/output/aws-cn/function_with_snapstart.json new file mode 100644 index 000000000..345a6d373 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_snapstart.json @@ -0,0 +1,204 @@ +{ + "Parameters": { + "SnapStartParam": { + "Default": "None", + "Type": "String" + } + }, + "Resources": { + "SnapStartFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAlias": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionWithAliasAliaslive": { + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasVersion0abd29242e", + "Version" + ] + }, + "Name": "live" + }, + "Type": "AWS::Lambda::Alias" + }, + "SnapStartFunctionWithAliasRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAliasVersion0abd29242e": { + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + } + }, + "Type": "AWS::Lambda::Version" + }, + "SnapStartParameterFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartParameterFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": { + "Ref": "SnapStartParam" + } + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartParameterFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/aws-cn/globals_for_function.json b/tests/translator/output/aws-cn/globals_for_function.json index 0b58482e8..5e3b7c066 100644 --- a/tests/translator/output/aws-cn/globals_for_function.json +++ b/tests/translator/output/aws-cn/globals_for_function.json @@ -37,6 +37,9 @@ ] }, "Runtime": "nodejs12.x", + "SnapStart": { + "ApplyOn": "None" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -165,6 +168,9 @@ ] }, "Runtime": "python2.7", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -197,7 +203,7 @@ }, "FunctionVersion": { "Fn::GetAtt": [ - "MinimalFunctionVersion0a06fc8fb1", + "MinimalFunctionVersione7c6f56e4d", "Version" ] }, @@ -242,7 +248,7 @@ }, "Type": "AWS::IAM::Role" }, - "MinimalFunctionVersion0a06fc8fb1": { + "MinimalFunctionVersione7c6f56e4d": { "DeletionPolicy": "Retain", "Properties": { "FunctionName": { diff --git a/tests/translator/output/aws-us-gov/function_with_snapstart.json b/tests/translator/output/aws-us-gov/function_with_snapstart.json new file mode 100644 index 000000000..bc5e828c9 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_snapstart.json @@ -0,0 +1,204 @@ +{ + "Parameters": { + "SnapStartParam": { + "Default": "None", + "Type": "String" + } + }, + "Resources": { + "SnapStartFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAlias": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionWithAliasAliaslive": { + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasVersion0abd29242e", + "Version" + ] + }, + "Name": "live" + }, + "Type": "AWS::Lambda::Alias" + }, + "SnapStartFunctionWithAliasRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAliasVersion0abd29242e": { + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + } + }, + "Type": "AWS::Lambda::Version" + }, + "SnapStartParameterFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartParameterFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": { + "Ref": "SnapStartParam" + } + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartParameterFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/aws-us-gov/globals_for_function.json b/tests/translator/output/aws-us-gov/globals_for_function.json index 851c36740..1f22a2a28 100644 --- a/tests/translator/output/aws-us-gov/globals_for_function.json +++ b/tests/translator/output/aws-us-gov/globals_for_function.json @@ -37,6 +37,9 @@ ] }, "Runtime": "nodejs12.x", + "SnapStart": { + "ApplyOn": "None" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -165,6 +168,9 @@ ] }, "Runtime": "python2.7", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -197,7 +203,7 @@ }, "FunctionVersion": { "Fn::GetAtt": [ - "MinimalFunctionVersion0a06fc8fb1", + "MinimalFunctionVersione7c6f56e4d", "Version" ] }, @@ -242,7 +248,7 @@ }, "Type": "AWS::IAM::Role" }, - "MinimalFunctionVersion0a06fc8fb1": { + "MinimalFunctionVersione7c6f56e4d": { "DeletionPolicy": "Retain", "Properties": { "FunctionName": { diff --git a/tests/translator/output/function_with_snapstart.json b/tests/translator/output/function_with_snapstart.json new file mode 100644 index 000000000..0d8fb81b9 --- /dev/null +++ b/tests/translator/output/function_with_snapstart.json @@ -0,0 +1,204 @@ +{ + "Parameters": { + "SnapStartParam": { + "Default": "None", + "Type": "String" + } + }, + "Resources": { + "SnapStartFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAlias": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartFunctionWithAliasAliaslive": { + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "SnapStartFunctionWithAliasVersion0abd29242e", + "Version" + ] + }, + "Name": "live" + }, + "Type": "AWS::Lambda::Alias" + }, + "SnapStartFunctionWithAliasRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "SnapStartFunctionWithAliasVersion0abd29242e": { + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "SnapStartFunctionWithAlias" + } + }, + "Type": "AWS::Lambda::Version" + }, + "SnapStartParameterFunction": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "SnapStartParameterFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "SnapStart": { + "ApplyOn": { + "Ref": "SnapStartParam" + } + }, + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "SnapStartParameterFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/globals_for_function.json b/tests/translator/output/globals_for_function.json index 6668b0fb4..7dfd8d294 100644 --- a/tests/translator/output/globals_for_function.json +++ b/tests/translator/output/globals_for_function.json @@ -37,6 +37,9 @@ ] }, "Runtime": "nodejs12.x", + "SnapStart": { + "ApplyOn": "None" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -165,6 +168,9 @@ ] }, "Runtime": "python2.7", + "SnapStart": { + "ApplyOn": "PublishedVersions" + }, "Tags": [ { "Key": "lambda:createdBy", @@ -197,7 +203,7 @@ }, "FunctionVersion": { "Fn::GetAtt": [ - "MinimalFunctionVersion0a06fc8fb1", + "MinimalFunctionVersione7c6f56e4d", "Version" ] }, @@ -242,7 +248,7 @@ }, "Type": "AWS::IAM::Role" }, - "MinimalFunctionVersion0a06fc8fb1": { + "MinimalFunctionVersione7c6f56e4d": { "DeletionPolicy": "Retain", "Properties": { "FunctionName": { diff --git a/tests/translator/test_function_resources.py b/tests/translator/test_function_resources.py index bb3c8e5cc..fbfc4ebca 100644 --- a/tests/translator/test_function_resources.py +++ b/tests/translator/test_function_resources.py @@ -714,6 +714,28 @@ def test_version_logical_id_changes_with_intrinsic_functions(self, LogicalIdGene LogicalIdGeneratorMock.assert_called_with(prefix, new_code, None) self.intrinsics_resolver_mock.resolve_parameter_refs.assert_called_with(self.lambda_func.Code) + def test_version_logical_id_changes_with_snapstart(self): + id_val = "LogicalId" + lambda_func = self._make_lambda_function(id_val) + + lambda_func_snapstart = self._make_lambda_function(id_val) + lambda_func_snapstart.SnapStart = {"ApplyOn": "PublishedVersions"} + + lambda_func_snapstart_none = self._make_lambda_function(id_val) + lambda_func_snapstart_none.SnapStart = {"ApplyOn": "None"} + + self.intrinsics_resolver_mock.resolve_parameter_refs.return_value = lambda_func.Code + + version1 = self.sam_func._construct_version(lambda_func, self.intrinsics_resolver_mock) + version_snapstart = self.sam_func._construct_version(lambda_func_snapstart, self.intrinsics_resolver_mock) + version_snapstart_none = self.sam_func._construct_version( + lambda_func_snapstart_none, + self.intrinsics_resolver_mock, + ) + # SnapStart config changes the hash, except when ApplyOn is "None" + self.assertNotEqual(version1.logical_id, version_snapstart.logical_id) + self.assertEqual(version1.logical_id, version_snapstart_none.logical_id) + def test_alias_creation(self): name = "aliasname" From b23646c205163fc15184df3ef49bdb53170f1bfd Mon Sep 17 00:00:00 2001 From: aws-sam-cli-bot <46753707+aws-sam-cli-bot@users.noreply.github.com> Date: Tue, 29 Nov 2022 03:13:58 +0000 Subject: [PATCH 4/4] chore: bump version to 1.55.0 --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index aff1b006f..4e9b170d0 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.54.0" +__version__ = "1.55.0"