def _construct_route53_recordsetgroup(self):
        record_set_group = None
        if self.domain.get("Route53") is not None:
            route53 = self.domain.get("Route53")
            if route53.get("HostedZoneId") is None and route53.get(
                    "HostedZoneName") is None:
                raise InvalidResourceException(
                    self.logical_id,
                    "HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
                )
            logical_id = logical_id_generator.LogicalIdGenerator(
                "",
                route53.get("HostedZoneId")
                or route53.get("HostedZoneName")).gen()
            record_set_group = Route53RecordSetGroup(
                "RecordSetGroup" + 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")
            record_set_group.RecordSets = self._construct_record_sets_for_domain(
                self.domain)

        return record_set_group
Пример #2
0
    def _construct_permission(
            self, function, source_arn=None, source_account=None, suffix="", event_source_token=None, prefix=None):
        """Constructs the Lambda Permission resource allowing the source service to invoke the function this event
        source triggers.

        :returns: the permission resource
        :rtype: model.lambda_.LambdaPermission
        """
        if prefix is None:
            prefix = self.logical_id
        if suffix.isalnum():
            permission_logical_id = prefix + 'Permission' + suffix
        else:
            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())
        try:
            # Name will not be available for Alias resources
            function_name_or_arn = function.get_runtime_attr("name")
        except NotImplementedError:
            function_name_or_arn = function.get_runtime_attr("arn")

        lambda_permission.Action = 'lambda:invokeFunction'
        lambda_permission.FunctionName = function_name_or_arn
        lambda_permission.Principal = self.principal
        lambda_permission.SourceArn = source_arn
        lambda_permission.SourceAccount = source_account
        lambda_permission.EventSourceToken = event_source_token

        return lambda_permission
    def _construct_stage(self):
        """Constructs and returns the ApiGatewayV2 Stage.

        :returns: the Stage to which this SAM Api corresponds
        :rtype: model.apigatewayv2.ApiGatewayV2Stage
        """

        # If there are no special configurations, don't create a stage and use the default
        if not self.stage_name and not self.stage_variables and not self.access_log_settings:
            return

        # 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
        stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else ""
        if stage_name_prefix.isalnum():
            stage_logical_id = self.logical_id + stage_name_prefix + "Stage"
        elif stage_name_prefix == DefaultStageName:
            stage_logical_id = self.logical_id + "ApiGatewayDefaultStage"
        else:
            generator = logical_id_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
        stage.StageVariables = self.stage_variables
        stage.AccessLogSettings = self.access_log_settings
        stage.AutoDeploy = True

        if self.tags is not None:
            stage.Tags = get_tag_list(self.tags)

        return stage
    def make_auto_deployable(self, stage, openapi_version=None, swagger=None):
        """
        Sets up the resource such that it will trigger a re-deployment when Swagger changes
        or the openapi version changes.

        :param swagger: Dictionary containing the Swagger definition of the API
        """
        if not swagger:
            return

        # CloudFormation does NOT redeploy the API unless it has a new deployment resource
        # that points to latest RestApi resource. Append a hash of Swagger Body location to
        # redeploy only when the API data changes. First 10 characters of hash is good enough
        # to prevent redeployment when API has not changed

        # NOTE: `str(swagger)` is for backwards compatibility. Changing it to a JSON or something will break compat
        hash_input = [str(swagger)]
        if openapi_version:
            hash_input.append(str(openapi_version))

        data = self._X_HASH_DELIMITER.join(hash_input)
        generator = logical_id_generator.LogicalIdGenerator(
            self.logical_id, data)
        self.logical_id = generator.gen()
        digest = generator.get_hash(length=40)  # Get the full hash
        self.Description = "RestApi deployment id: {}".format(digest)
        stage.update_deployment_ref(self.logical_id)
Пример #5
0
    def _construct_api_domain(self, http_api):
        """
        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:
            raise InvalidResourceException(
                self.logical_id,
                "Custom Domains only works if both DomainName and CertificateArn"
                " are provided.")

        self.domain["ApiDomainName"] = "{}{}".format(
            "ApiGatewayDomainNameV2",
            logical_id_generator.LogicalIdGenerator(
                "", self.domain.get("DomainName")).gen())

        domain = ApiGatewayV2DomainName(
            self.domain.get("ApiDomainName"),
            attributes=self.passthrough_resource_attributes)
        domain_config = dict()
        domain.DomainName = self.domain.get("DomainName")
        domain.Tags = self.tags
        endpoint = self.domain.get("EndpointConfiguration")

        if endpoint is None:
            endpoint = "REGIONAL"
            # to make sure that default is always REGIONAL
            self.domain["EndpointConfiguration"] = "REGIONAL"
        elif endpoint not in ["REGIONAL"]:
            raise InvalidResourceException(
                self.logical_id,
                "EndpointConfiguration for Custom Domains must be one of {}.".
                format(["REGIONAL"]),
            )
        domain_config["EndpointType"] = endpoint
        domain_config["CertificateArn"] = self.domain.get("CertificateArn")

        domain.DomainNameConfigurations = [domain_config]

        # Create BasepathMappings
        if self.domain.get("BasePath") and isinstance(
                self.domain.get("BasePath"), string_types):
            basepaths = [self.domain.get("BasePath")]
        elif self.domain.get("BasePath") and isinstance(
                self.domain.get("BasePath"), list):
            basepaths = self.domain.get("BasePath")
        else:
            basepaths = None
        basepath_resource_list = self._construct_basepath_mappings(
            basepaths, http_api)

        # Create the Route53 RecordSetGroup resource
        record_set_group = self._construct_route53_recordsetgroup()

        return domain, basepath_resource_list, record_set_group
Пример #6
0
    def _construct_version(self, function, intrinsics_resolver):
        """Constructs a Lambda Version resource that will be auto-published when CodeUri of the function changes.
        Old versions will not be deleted without a direct reference from the CloudFormation template.

        :param model.lambda_.LambdaFunction function: Lambda function object that is being connected to a version
        :param model.intrinsics.resolver.IntrinsicsResolver intrinsics_resolver: Class that can help resolve
            references to parameters present in CodeUri. It is a common usecase to set S3Key of Code to be a
            template parameter. Need to resolve the values otherwise we will never detect a change in Code dict
        :return: Lambda function Version resource
        """
        code_dict = function.Code
        if not code_dict:
            raise ValueError(
                "Lambda function code must be a valid non-empty dictionary")

        if not intrinsics_resolver:
            raise ValueError(
                "intrinsics_resolver is required for versions creation")

        # Resolve references to template parameters before creating hash. This will *not* resolve all intrinsics
        # because we cannot resolve runtime values like Arn of a resource. For purposes of detecting changes, this
        # is good enough. Here is why:
        #
        # When using intrinsic functions there are two cases when has must change:
        #   - Value of the template parameter changes
        #   - (or) LogicalId of a referenced resource changes ie. !GetAtt NewResource.Arn
        #
        # Later case will already change the hash because some value in the Code dictionary changes. We handle the
        # first case by resolving references to template parameters. It is okay even if these references are
        # present inside another intrinsic such as !Join. The resolver will replace the reference with the parameter's
        # value and keep all other parts of !Join identical. This will still trigger a change in the hash.
        code_dict = intrinsics_resolver.resolve_parameter_refs(code_dict)

        # Construct the LogicalID of Lambda version by appending 10 characters of SHA of CodeUri. This is necessary
        # to trigger creation of a new version every time code location changes. Since logicalId changes, CloudFormation
        # will drop the old version and create a new one for us. We set a DeletionPolicy on the version resource to
        # prevent CloudFormation from actually deleting the underlying version resource
        #
        # SHA Collisions: For purposes of triggering a new update, we are concerned about just the difference previous
        #                 and next hashes. The chances that two subsequent hashes collide is fairly low.
        prefix = "{id}Version".format(id=self.logical_id)
        logical_id = logical_id_generator.LogicalIdGenerator(
            prefix, code_dict).gen()

        attributes = self.get_passthrough_resource_attributes()
        if attributes is None:
            attributes = {}
        attributes["DeletionPolicy"] = "Retain"

        lambda_version = LambdaVersion(logical_id=logical_id,
                                       attributes=attributes)
        lambda_version.FunctionName = function.get_runtime_attr('name')
        lambda_version.Description = self.VersionDescription

        return lambda_version
Пример #7
0
    def _construct_lambda_layer(self, intrinsics_resolver):
        """Constructs and returns the Lambda function.

        :returns: a list containing the Lambda function and execution role resources
        :rtype: list
        """
        # Resolve intrinsics if applicable:
        self.LayerName = self._resolve_string_parameter(
            intrinsics_resolver, self.LayerName, 'LayerName')
        self.LicenseInfo = self._resolve_string_parameter(
            intrinsics_resolver, self.LicenseInfo, 'LicenseInfo')
        self.Description = self._resolve_string_parameter(
            intrinsics_resolver, self.Description, 'Description')
        self.RetentionPolicy = self._resolve_string_parameter(
            intrinsics_resolver, self.RetentionPolicy, 'RetentionPolicy')

        retention_policy_value = self._get_retention_policy_value()

        attributes = self.get_passthrough_resource_attributes()
        if attributes is None:
            attributes = {}
        attributes['DeletionPolicy'] = retention_policy_value

        old_logical_id = self.logical_id
        new_logical_id = logical_id_generator.LogicalIdGenerator(
            old_logical_id, self.to_dict()).gen()
        self.logical_id = new_logical_id

        lambda_layer = LambdaLayerVersion(self.logical_id,
                                          depends_on=self.depends_on,
                                          attributes=attributes)

        # Changing the LayerName property: when a layer is published, it is given an Arn
        # example: arn:aws:lambda:us-west-2:123456789012:layer:MyLayer:1
        # where MyLayer is the LayerName property if it exists; otherwise, it is the
        # LogicalId of this resource. Since a LayerVersion is an immutable resource, when
        # CloudFormation updates this resource, it will ALWAYS create a new version then
        # delete the old version if the logical ids match. What this does is change the
        # logical id of every layer (so a `DeletionPolicy: Retain` can work) and set the
        # LayerName property of the layer so that the Arn will still always be the same
        # with the exception of an incrementing version number.
        if not self.LayerName:
            self.LayerName = old_logical_id

        lambda_layer.LayerName = self.LayerName
        lambda_layer.Description = self.Description
        lambda_layer.Content = construct_s3_location_object(
            self.ContentUri, self.logical_id, 'ContentUri')
        lambda_layer.CompatibleRuntimes = self.CompatibleRuntimes
        lambda_layer.LicenseInfo = self.LicenseInfo

        return lambda_layer
Пример #8
0
    def make_auto_deployable(self,
                             stage,
                             openapi_version=None,
                             swagger=None,
                             domain=None,
                             redeploy_restapi_parameters=None):
        """
        Sets up the resource such that it will trigger a re-deployment when Swagger changes
        or the openapi version changes or a domain resource changes.

        :param swagger: Dictionary containing the Swagger definition of the API
        :param openapi_version: string containing value of OpenApiVersion flag in the template
        :param domain: Dictionary containing the custom domain configuration for the API
        :param redeploy_restapi_parameters: Dictionary containing the properties for which rest api will be redeployed
        """
        if not swagger:
            return

        # CloudFormation does NOT redeploy the API unless it has a new deployment resource
        # that points to latest RestApi resource. Append a hash of Swagger Body location to
        # redeploy only when the API data changes. First 10 characters of hash is good enough
        # to prevent redeployment when API has not changed

        # NOTE: `str(swagger)` is for backwards compatibility. Changing it to a JSON or something will break compat
        hash_input = [str(swagger)]
        if openapi_version:
            hash_input.append(str(openapi_version))
        if domain:
            hash_input.append(json.dumps(domain))
        if redeploy_restapi_parameters:
            function_names = redeploy_restapi_parameters.get("function_names")
        else:
            function_names = None
        # The deployment logical id is <api logicalId> + "Deployment"
        # The keyword "Deployment" is removed and all the function names associated with api is obtained
        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)
        self.logical_id = generator.gen()
        digest = generator.get_hash(length=40)  # Get the full hash
        self.Description = "RestApi deployment id: {}".format(digest)
        stage.update_deployment_ref(self.logical_id)
Пример #9
0
    def _generate_logical_id(self, prefix, suffix, resource_type):
        """Helper utility to generate a logicial ID for a new resource

        :param string prefix: Prefix to use for the logical ID of the resource
        :param string suffix: Suffix to add for the logical ID of the resource
        :param string resource_type: Type of the resource

        :returns: the logical ID for the new resource
        :rtype: string
        """
        if prefix is None:
            prefix = self.logical_id
        if suffix.isalnum():
            logical_id = prefix + resource_type + suffix
        else:
            generator = logical_id_generator.LogicalIdGenerator(
                prefix + resource_type, suffix)
            logical_id = generator.gen()
        return logical_id
    def _construct_stage(self, deployment, swagger,
                         redeploy_restapi_parameters):
        """Constructs and returns the ApiGateway Stage.

        :param model.apigateway.ApiGatewayDeployment deployment: the Deployment for this Stage
        :returns: the Stage to which this SAM Api corresponds
        :rtype: model.apigateway.ApiGatewayStage
        """

        # 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
        stage_name_prefix = self.stage_name if isinstance(
            self.stage_name, string_types) else ""
        if stage_name_prefix.isalnum():
            stage_logical_id = self.logical_id + stage_name_prefix + "Stage"
        else:
            generator = logical_id_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)
        stage.StageName = self.stage_name
        stage.CacheClusterEnabled = self.cache_cluster_enabled
        stage.CacheClusterSize = self.cache_cluster_size
        stage.Variables = self.variables
        stage.MethodSettings = self.method_settings
        stage.AccessLogSetting = self.access_log_setting
        stage.CanarySetting = self.canary_setting
        stage.TracingEnabled = self.tracing_enabled

        if swagger is not None:
            deployment.make_auto_deployable(stage, self.remove_extra_stage,
                                            swagger, self.domain,
                                            redeploy_restapi_parameters)

        if self.tags is not None:
            stage.Tags = get_tag_list(self.tags)

        return stage
    def _construct_api_domain(self, rest_api):
        """
        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:
            raise InvalidResourceException(
                self.logical_id, "Custom Domains only works if both DomainName and CertificateArn" " are provided."
            )

        self.domain["ApiDomainName"] = "{}{}".format(
            "ApiGatewayDomainName", logical_id_generator.LogicalIdGenerator("", self.domain.get("DomainName")).gen()
        )

        domain = ApiGatewayDomainName(self.domain.get("ApiDomainName"), attributes=self.passthrough_resource_attributes)
        domain.DomainName = self.domain.get("DomainName")
        endpoint = self.domain.get("EndpointConfiguration")

        if endpoint is None:
            endpoint = "REGIONAL"
            self.domain["EndpointConfiguration"] = "REGIONAL"
        elif endpoint not in ["EDGE", "REGIONAL", "PRIVATE"]:
            raise InvalidResourceException(
                self.logical_id,
                "EndpointConfiguration for Custom Domains must be"
                " one of {}.".format(["EDGE", "REGIONAL", "PRIVATE"]),
            )

        if endpoint == "REGIONAL":
            domain.RegionalCertificateArn = self.domain.get("CertificateArn")
        else:
            domain.CertificateArn = self.domain.get("CertificateArn")

        domain.EndpointConfiguration = {"Types": [endpoint]}

        mutual_tls_auth = self.domain.get("MutualTlsAuthentication", None)
        if mutual_tls_auth:
            if isinstance(mutual_tls_auth, dict):
                if not set(mutual_tls_auth.keys()).issubset({"TruststoreUri", "TruststoreVersion"}):
                    invalid_keys = list()
                    for key in mutual_tls_auth.keys():
                        if not key in {"TruststoreUri", "TruststoreVersion"}:
                            invalid_keys.append(key)
                    invalid_keys.sort()
                    raise InvalidResourceException(
                        ",".join(invalid_keys),
                        "Available MutualTlsAuthentication fields are {}.".format(
                            ["TruststoreUri", "TruststoreVersion"]
                        ),
                    )
                domain.MutualTlsAuthentication = {}
                if mutual_tls_auth.get("TruststoreUri", None):
                    domain.MutualTlsAuthentication["TruststoreUri"] = mutual_tls_auth["TruststoreUri"]
                if mutual_tls_auth.get("TruststoreVersion", None):
                    domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"]
            else:
                raise InvalidResourceException(
                    mutual_tls_auth,
                    "MutualTlsAuthentication must be a map with at least one of the following fields {}.".format(
                        ["TruststoreUri", "TruststoreVersion"]
                    ),
                )

        if self.domain.get("SecurityPolicy", None):
            domain.SecurityPolicy = self.domain["SecurityPolicy"]

        # Create BasepathMappings
        if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), string_types):
            basepaths = [self.domain.get("BasePath")]
        elif self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), list):
            basepaths = self.domain.get("BasePath")
        else:
            basepaths = None

        basepath_resource_list = []

        if basepaths is None:
            basepath_mapping = ApiGatewayBasePathMapping(
                self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes
            )
            basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName"))
            basepath_mapping.RestApiId = ref(rest_api.logical_id)
            basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
            basepath_resource_list.extend([basepath_mapping])
        else:
            for path in basepaths:
                path = "".join(e for e in path if e.isalnum())
                logical_id = "{}{}{}".format(self.logical_id, path, "BasePathMapping")
                basepath_mapping = ApiGatewayBasePathMapping(
                    logical_id, attributes=self.passthrough_resource_attributes
                )
                basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName"))
                basepath_mapping.RestApiId = ref(rest_api.logical_id)
                basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
                basepath_mapping.BasePath = path
                basepath_resource_list.extend([basepath_mapping])

        # Create the Route53 RecordSetGroup resource
        record_set_group = None
        if self.domain.get("Route53") is not None:
            route53 = self.domain.get("Route53")
            if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None:
                raise InvalidResourceException(
                    self.logical_id,
                    "HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
                )
            logical_id = logical_id_generator.LogicalIdGenerator(
                "", route53.get("HostedZoneId") or route53.get("HostedZoneName")
            ).gen()
            record_set_group = Route53RecordSetGroup(
                "RecordSetGroup" + logical_id, attributes=self.passthrough_resource_attributes
            )
            if "HostedZoneId" in route53:
                record_set_group.HostedZoneId = route53.get("HostedZoneId")
            if "HostedZoneName" in route53:
                record_set_group.HostedZoneName = route53.get("HostedZoneName")
            record_set_group.RecordSets = self._construct_record_sets_for_domain(self.domain)

        return domain, basepath_resource_list, record_set_group
    def _construct_api_domain(self, rest_api):
        """
        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:
            raise InvalidResourceException(
                self.logical_id,
                "Custom Domains only works if both DomainName and CertificateArn"
                " are provided")

        self.domain["ApiDomainName"] = "{}{}".format(
            "ApiGatewayDomainName",
            logical_id_generator.LogicalIdGenerator(
                "", self.domain.get("DomainName")).gen())

        domain = ApiGatewayDomainName(
            self.domain.get("ApiDomainName"),
            attributes=self.passthrough_resource_attributes)
        domain.DomainName = self.domain.get("DomainName")
        endpoint = self.domain.get("EndpointConfiguration")

        if endpoint is None:
            endpoint = "REGIONAL"
            self.domain["EndpointConfiguration"] = "REGIONAL"
        elif endpoint not in ["EDGE", "REGIONAL"]:
            raise InvalidResourceException(
                self.logical_id,
                "EndpointConfiguration for Custom Domains must be"
                " one of {}".format(["EDGE", "REGIONAL"]),
            )

        if endpoint == "REGIONAL":
            domain.RegionalCertificateArn = self.domain.get("CertificateArn")
        else:
            domain.CertificateArn = self.domain.get("CertificateArn")

        domain.EndpointConfiguration = {"Types": [endpoint]}

        # Create BasepathMappings
        if self.domain.get("BasePath") and isinstance(
                self.domain.get("BasePath"), string_types):
            basepaths = [self.domain.get("BasePath")]
        elif self.domain.get("BasePath") and isinstance(
                self.domain.get("BasePath"), list):
            basepaths = self.domain.get("BasePath")
        else:
            basepaths = None

        basepath_resource_list = []

        if basepaths is None:
            basepath_mapping = ApiGatewayBasePathMapping(
                self.logical_id + "BasePathMapping",
                attributes=self.passthrough_resource_attributes)
            basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName"))
            basepath_mapping.RestApiId = ref(rest_api.logical_id)
            basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
            basepath_resource_list.extend([basepath_mapping])
        else:
            for path in basepaths:
                path = "".join(e for e in path if e.isalnum())
                logical_id = "{}{}{}".format(self.logical_id, path,
                                             "BasePathMapping")
                basepath_mapping = ApiGatewayBasePathMapping(
                    logical_id,
                    attributes=self.passthrough_resource_attributes)
                basepath_mapping.DomainName = ref(
                    self.domain.get("ApiDomainName"))
                basepath_mapping.RestApiId = ref(rest_api.logical_id)
                basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
                basepath_mapping.BasePath = path
                basepath_resource_list.extend([basepath_mapping])

        # Create the Route53 RecordSetGroup resource
        record_set_group = None
        if self.domain.get("Route53") is not None:
            route53 = self.domain.get("Route53")
            if route53.get("HostedZoneId") is None:
                raise InvalidResourceException(
                    self.logical_id,
                    "HostedZoneId is required to enable Route53 support on Custom Domains."
                )
            logical_id = logical_id_generator.LogicalIdGenerator(
                "", route53.get("HostedZoneId")).gen()
            record_set_group = Route53RecordSetGroup(
                "RecordSetGroup" + logical_id,
                attributes=self.passthrough_resource_attributes)
            record_set_group.HostedZoneId = route53.get("HostedZoneId")
            record_set_group.RecordSets = self._construct_record_sets_for_domain(
                self.domain)

        return domain, basepath_resource_list, record_set_group
    def _construct_api_domain(self, http_api):
        """
        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:
            raise InvalidResourceException(
                self.logical_id,
                "Custom Domains only works if both DomainName and CertificateArn"
                " are provided.")

        self.domain["ApiDomainName"] = "{}{}".format(
            "ApiGatewayDomainNameV2",
            logical_id_generator.LogicalIdGenerator(
                "", self.domain.get("DomainName")).gen())

        domain = ApiGatewayV2DomainName(
            self.domain.get("ApiDomainName"),
            attributes=self.passthrough_resource_attributes)
        domain_config = dict()
        domain.DomainName = self.domain.get("DomainName")
        domain.Tags = self.tags
        endpoint = self.domain.get("EndpointConfiguration")

        if endpoint is None:
            endpoint = "REGIONAL"
            # to make sure that default is always REGIONAL
            self.domain["EndpointConfiguration"] = "REGIONAL"
        elif endpoint not in ["REGIONAL"]:
            raise InvalidResourceException(
                self.logical_id,
                "EndpointConfiguration for Custom Domains must be one of {}.".
                format(["REGIONAL"]),
            )
        domain_config["EndpointType"] = endpoint
        domain_config["CertificateArn"] = self.domain.get("CertificateArn")
        if self.domain.get("SecurityPolicy", None):
            domain_config["SecurityPolicy"] = self.domain.get("SecurityPolicy")

        domain.DomainNameConfigurations = [domain_config]

        mutual_tls_auth = self.domain.get("MutualTlsAuthentication", None)
        if mutual_tls_auth:
            if isinstance(mutual_tls_auth, dict):
                if not set(mutual_tls_auth.keys()).issubset(
                    {"TruststoreUri", "TruststoreVersion"}):
                    invalid_keys = []
                    for key in mutual_tls_auth.keys():
                        if key not in {"TruststoreUri", "TruststoreVersion"}:
                            invalid_keys.append(key)
                    invalid_keys.sort()
                    raise InvalidResourceException(
                        ",".join(invalid_keys),
                        "Available MutualTlsAuthentication fields are {}.".
                        format(["TruststoreUri", "TruststoreVersion"]),
                    )
                domain.MutualTlsAuthentication = {}
                if mutual_tls_auth.get("TruststoreUri", None):
                    domain.MutualTlsAuthentication[
                        "TruststoreUri"] = mutual_tls_auth["TruststoreUri"]
                if mutual_tls_auth.get("TruststoreVersion", None):
                    domain.MutualTlsAuthentication[
                        "TruststoreVersion"] = mutual_tls_auth[
                            "TruststoreVersion"]
            else:
                raise InvalidResourceException(
                    mutual_tls_auth,
                    "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"), string_types):
            basepaths = [self.domain.get("BasePath")]
        elif self.domain.get("BasePath") and isinstance(
                self.domain.get("BasePath"), list):
            basepaths = self.domain.get("BasePath")
        else:
            basepaths = None
        basepath_resource_list = self._construct_basepath_mappings(
            basepaths, http_api)

        # Create the Route53 RecordSetGroup resource
        record_set_group = self._construct_route53_recordsetgroup()

        return domain, basepath_resource_list, record_set_group