diff --git a/.cfnlintrc.yaml b/.cfnlintrc.yaml index de9d8e4025..b044091ccf 100644 --- a/.cfnlintrc.yaml +++ b/.cfnlintrc.yaml @@ -139,6 +139,7 @@ ignore_templates: - tests/translator/output/**/managed_policies_everything.json # intentionally contains wrong arns - tests/translator/output/**/function_with_provisioned_poller_config.json - tests/translator/output/**/function_with_metrics_config.json + - tests/translator/output/**/api_with_custom_domains_private.json ignore_checks: - E2531 # Deprecated runtime; not relevant for transform tests diff --git a/samtranslator/internal/schema_source/aws_serverless_api.py b/samtranslator/internal/schema_source/aws_serverless_api.py index f4a7471c4e..bb315b2d67 100644 --- a/samtranslator/internal/schema_source/aws_serverless_api.py +++ b/samtranslator/internal/schema_source/aws_serverless_api.py @@ -159,13 +159,16 @@ class Route53(BaseModel): class Domain(BaseModel): BasePath: Optional[PassThroughProp] = domain("BasePath") NormalizeBasePath: Optional[bool] = domain("NormalizeBasePath") + Policy: Optional[PassThroughProp] CertificateArn: PassThroughProp = domain("CertificateArn") DomainName: PassThroughProp = passthrough_prop( DOMAIN_STEM, "DomainName", ["AWS::ApiGateway::DomainName", "Properties", "DomainName"], ) - EndpointConfiguration: Optional[SamIntrinsicable[Literal["REGIONAL", "EDGE"]]] = domain("EndpointConfiguration") + EndpointConfiguration: Optional[SamIntrinsicable[Literal["REGIONAL", "EDGE", "PRIVATE"]]] = domain( + "EndpointConfiguration" + ) MutualTlsAuthentication: Optional[PassThroughProp] = passthrough_prop( DOMAIN_STEM, "MutualTlsAuthentication", diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 92a360c115..1bad8d41c2 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -10,8 +10,10 @@ ApiGatewayApiKey, ApiGatewayAuthorizer, ApiGatewayBasePathMapping, + ApiGatewayBasePathMappingV2, ApiGatewayDeployment, ApiGatewayDomainName, + ApiGatewayDomainNameV2, ApiGatewayResponse, ApiGatewayRestApi, ApiGatewayStage, @@ -79,6 +81,13 @@ class ApiDomainResponse: recordset_group: Any +@dataclass +class ApiDomainResponseV2: + domain: Optional[ApiGatewayDomainNameV2] + apigw_basepath_mapping_list: Optional[List[ApiGatewayBasePathMappingV2]] + recordset_group: Any + + class SharedApiUsagePlan: """ Collects API information from different API resources in the same template, @@ -517,11 +526,7 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915 if mutual_tls_auth.get("TruststoreVersion", None): domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"] - if self.domain.get("SecurityPolicy", None): - domain.SecurityPolicy = self.domain["SecurityPolicy"] - - if self.domain.get("OwnershipVerificationCertificateArn", None): - domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"] + self._set_optional_domain_properties(domain) basepaths: Optional[List[str]] basepath_value = self.domain.get("BasePath") @@ -539,12 +544,102 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915 basepath_resource_list: List[ApiGatewayBasePathMapping] = [] if basepaths is None: - basepath_mapping = ApiGatewayBasePathMapping( - self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes + basepath_mapping = self._create_basepath_mapping(api_domain_name, rest_api, None, None) + basepath_resource_list.extend([basepath_mapping]) + else: + sam_expect(basepaths, self.logical_id, "Domain.BasePath").to_be_a_list_of(ExpectedType.STRING) + for basepath in basepaths: + # Remove possible leading and trailing '/' because a base path may only + # contain letters, numbers, and one of "$-_.+!*'()" + path = "".join(e for e in basepath if e.isalnum()) + mapping_basepath = path if normalize_basepath else basepath + logical_id = "{}{}{}".format(self.logical_id, path, "BasePathMapping") + basepath_mapping = self._create_basepath_mapping( + api_domain_name, rest_api, logical_id, mapping_basepath + ) + basepath_resource_list.extend([basepath_mapping]) + + # Create the Route53 RecordSetGroup resource + record_set_group = None + route53 = self.domain.get("Route53") + if route53 is not None: + sam_expect(route53, self.logical_id, "Domain.Route53").to_be_a_map() + 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_suffix = LogicalIdGenerator( + "", route53.get("HostedZoneId") or route53.get("HostedZoneName") + ).gen() + logical_id = "RecordSetGroup" + logical_id_suffix + + record_set_group = route53_record_set_groups.get(logical_id) + + if route53.get("SeparateRecordSetGroup"): + sam_expect( + route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup" + ).to_be_a_bool() + return ApiDomainResponse( + domain, + basepath_resource_list, + self._construct_single_record_set_group(self.domain, api_domain_name, route53), + ) + + if not record_set_group: + record_set_group = self._get_record_set_group(logical_id, route53) + route53_record_set_groups[logical_id] = record_set_group + + record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, api_domain_name, route53) + + return ApiDomainResponse(domain, basepath_resource_list, record_set_group) + + def _construct_api_domain_v2( + self, rest_api: ApiGatewayRestApi, route53_record_set_groups: Any + ) -> ApiDomainResponseV2: + """ + Constructs and returns the ApiGateway Domain V2 and BasepathMapping V2 + """ + if self.domain is None: + return ApiDomainResponseV2(None, None, None) + + sam_expect(self.domain, self.logical_id, "Domain").to_be_a_map() + domain_name: PassThrough = sam_expect( + self.domain.get("DomainName"), self.logical_id, "Domain.DomainName" + ).to_not_be_none() + certificate_arn: PassThrough = sam_expect( + self.domain.get("CertificateArn"), self.logical_id, "Domain.CertificateArn" + ).to_not_be_none() + + api_domain_name = "{}{}".format("ApiGatewayDomainNameV2", LogicalIdGenerator("", domain_name).gen()) + domain_name_arn = ref(api_domain_name) + domain = ApiGatewayDomainNameV2(api_domain_name, attributes=self.passthrough_resource_attributes) + + domain.DomainName = domain_name + endpoint = self.domain.get("EndpointConfiguration") + + if endpoint not in ["EDGE", "REGIONAL", "PRIVATE"]: + raise InvalidResourceException( + self.logical_id, + "EndpointConfiguration for Custom Domains must be" + " one of {}.".format(["EDGE", "REGIONAL", "PRIVATE"]), ) - basepath_mapping.DomainName = ref(api_domain_name) - basepath_mapping.RestApiId = ref(rest_api.logical_id) - basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage") + + domain.CertificateArn = certificate_arn + + domain.EndpointConfiguration = {"Types": [endpoint]} + + self._set_optional_domain_properties(domain) + + basepaths: Optional[List[str]] = self._get_basepaths() + + # Boolean to allow/disallow symbols in BasePath property + normalize_basepath = self.domain.get("NormalizeBasePath", True) + + basepath_resource_list: List[ApiGatewayBasePathMappingV2] = [] + if basepaths is None: + basepath_mapping = self._create_basepath_mapping_v2(domain_name_arn, rest_api) basepath_resource_list.extend([basepath_mapping]) else: sam_expect(basepaths, self.logical_id, "Domain.BasePath").to_be_a_list_of(ExpectedType.STRING) @@ -553,10 +648,10 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915 # contain letters, numbers, and one of "$-_.+!*'()" path = "".join(e for e in basepath if e.isalnum()) logical_id = "{}{}{}".format(self.logical_id, path, "BasePathMapping") - basepath_mapping = ApiGatewayBasePathMapping( + basepath_mapping = ApiGatewayBasePathMappingV2( logical_id, attributes=self.passthrough_resource_attributes ) - basepath_mapping.DomainName = ref(api_domain_name) + basepath_mapping.DomainNameArn = domain_name_arn basepath_mapping.RestApiId = ref(rest_api.logical_id) basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage") basepath_mapping.BasePath = path if normalize_basepath else basepath @@ -584,24 +679,48 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915 sam_expect( route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup" ).to_be_a_bool() - return ApiDomainResponse( + return ApiDomainResponseV2( domain, basepath_resource_list, - self._construct_single_record_set_group(self.domain, api_domain_name, route53), + self._construct_single_record_set_group(self.domain, domain_name, route53), ) if not record_set_group: - record_set_group = Route53RecordSetGroup(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 = [] + record_set_group = self._get_record_set_group(logical_id, route53) route53_record_set_groups[logical_id] = record_set_group - record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, api_domain_name, route53) + record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, domain_name, route53) - return ApiDomainResponse(domain, basepath_resource_list, record_set_group) + return ApiDomainResponseV2(domain, basepath_resource_list, record_set_group) + + def _get_basepaths(self) -> Optional[List[str]]: + if self.domain is None: + return None + basepath_value = self.domain.get("BasePath") + if self.domain.get("BasePath") and isinstance(basepath_value, str): + return [basepath_value] + if self.domain.get("BasePath") and isinstance(basepath_value, list): + return cast(Optional[List[Any]], basepath_value) + return None + + def _set_optional_domain_properties(self, domain: Union[ApiGatewayDomainName, ApiGatewayDomainNameV2]) -> None: + if self.domain is None: + return + if self.domain.get("SecurityPolicy", None): + domain.SecurityPolicy = self.domain["SecurityPolicy"] + if self.domain.get("Policy", None): + domain.Policy = self.domain["Policy"] + if self.domain.get("OwnershipVerificationCertificateArn", None): + domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"] + + def _get_record_set_group(self, logical_id: str, route53: Dict[str, Any]) -> Route53RecordSetGroup: + record_set_group = Route53RecordSetGroup(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 = [] + return record_set_group def _construct_single_record_set_group( self, domain: Dict[str, Any], api_domain_name: str, route53: Any @@ -667,6 +786,40 @@ def _construct_alias_target(self, domain: Dict[str, Any], api_domain_name: str, alias_target["DNSName"] = route53.get("DistributionDomainName") return alias_target + def _create_basepath_mapping( + self, + api_domain_name: PassThrough, + rest_api: ApiGatewayRestApi, + logical_id: Optional[str], + basepath: Optional[str], + ) -> ApiGatewayBasePathMapping: + + basepath_mapping: ApiGatewayBasePathMapping + basepath_mapping = ( + ApiGatewayBasePathMapping(logical_id, attributes=self.passthrough_resource_attributes) + if logical_id + else ApiGatewayBasePathMapping( + self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes + ) + ) + basepath_mapping.DomainName = ref(api_domain_name) + basepath_mapping.RestApiId = ref(rest_api.logical_id) + basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage") + if basepath: + basepath_mapping.BasePath = basepath + return basepath_mapping + + def _create_basepath_mapping_v2( + self, domain_name_arn: PassThrough, rest_api: ApiGatewayRestApi + ) -> ApiGatewayBasePathMappingV2: + basepath_mapping = ApiGatewayBasePathMappingV2( + self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes + ) + basepath_mapping.DomainNameArn = domain_name_arn + basepath_mapping.RestApiId = ref(rest_api.logical_id) + basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage") + return basepath_mapping + @cw_timer(prefix="Generator", name="Api") def to_cloudformation( self, redeploy_restapi_parameters: Optional[Any], route53_record_set_groups: Dict[str, Route53RecordSetGroup] @@ -676,10 +829,19 @@ def to_cloudformation( :returns: a tuple containing the RestApi, Deployment, and Stage for an empty Api. :rtype: tuple """ + api_domain_response: Union[ApiDomainResponseV2, ApiDomainResponse] + domain: Union[Resource, None] + basepath_mapping: Union[List[ApiGatewayBasePathMapping], List[ApiGatewayBasePathMappingV2], None] rest_api = self._construct_rest_api() - api_domain_response = self._construct_api_domain(rest_api, route53_record_set_groups) + api_domain_response = ( + self._construct_api_domain_v2(rest_api, route53_record_set_groups) + if isinstance(self.domain, dict) and self.domain.get("EndpointConfiguration") == "PRIVATE" + else self._construct_api_domain(rest_api, route53_record_set_groups) + ) + domain = api_domain_response.domain basepath_mapping = api_domain_response.apigw_basepath_mapping_list + route53_recordsetGroup = api_domain_response.recordset_group deployment = self._construct_deployment(rest_api) @@ -703,6 +865,7 @@ def to_cloudformation( Tuple[Resource], List[LambdaPermission], List[ApiGatewayBasePathMapping], + List[ApiGatewayBasePathMappingV2], ], ] = [] diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 7d0ab6ffc4..d8aeddb94e 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -230,6 +230,25 @@ class ApiGatewayDomainName(Resource): OwnershipVerificationCertificateArn: Optional[PassThrough] +class ApiGatewayDomainNameV2(Resource): + resource_type = "AWS::ApiGateway::DomainNameV2" + property_types = { + "DomainName": GeneratedProperty(), + "EndpointConfiguration": GeneratedProperty(), + "SecurityPolicy": GeneratedProperty(), + "CertificateArn": GeneratedProperty(), + "Tags": GeneratedProperty(), + "Policy": GeneratedProperty(), + } + + DomainName: PassThrough + EndpointConfiguration: Optional[PassThrough] + SecurityPolicy: Optional[PassThrough] + CertificateArn: Optional[PassThrough] + Tags: Optional[PassThrough] + Policy: Optional[PassThrough] + + class ApiGatewayBasePathMapping(Resource): resource_type = "AWS::ApiGateway::BasePathMapping" property_types = { @@ -240,6 +259,16 @@ class ApiGatewayBasePathMapping(Resource): } +class ApiGatewayBasePathMappingV2(Resource): + resource_type = "AWS::ApiGateway::BasePathMappingV2" + property_types = { + "BasePath": GeneratedProperty(), + "DomainNameArn": GeneratedProperty(), + "RestApiId": GeneratedProperty(), + "Stage": GeneratedProperty(), + } + + class ApiGatewayUsagePlan(Resource): resource_type = "AWS::ApiGateway::UsagePlan" property_types = { diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 29d55f3482..397b7666d9 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -53,6 +53,7 @@ ApiGatewayApiKey, ApiGatewayDeployment, ApiGatewayDomainName, + ApiGatewayDomainNameV2, ApiGatewayStage, ApiGatewayUsagePlan, ApiGatewayUsagePlanKey, @@ -1310,6 +1311,7 @@ class SamApi(SamResourceMacro): "Stage": ApiGatewayStage.resource_type, "Deployment": ApiGatewayDeployment.resource_type, "DomainName": ApiGatewayDomainName.resource_type, + "DomainNameV2": ApiGatewayDomainNameV2.resource_type, "UsagePlan": ApiGatewayUsagePlan.resource_type, "UsagePlanKey": ApiGatewayUsagePlanKey.resource_type, "ApiKey": ApiGatewayApiKey.resource_type, diff --git a/samtranslator/schema/schema.json b/samtranslator/schema/schema.json index f4a08880f2..b8dc13fb15 100644 --- a/samtranslator/schema/schema.json +++ b/samtranslator/schema/schema.json @@ -277250,7 +277250,8 @@ { "enum": [ "REGIONAL", - "EDGE" + "EDGE", + "PRIVATE" ], "type": "string" } @@ -277273,6 +277274,9 @@ "title": "OwnershipVerificationCertificateArn", "type": "string" }, + "Policy": { + "$ref": "#/definitions/PassThroughProp" + }, "Route53": { "allOf": [ { diff --git a/samtranslator/translator/verify_logical_id.py b/samtranslator/translator/verify_logical_id.py index 4dd27882c9..510995a83b 100644 --- a/samtranslator/translator/verify_logical_id.py +++ b/samtranslator/translator/verify_logical_id.py @@ -15,6 +15,8 @@ "AWS::Cognito::UserPool": "AWS::Cognito::UserPool", "AWS::ApiGateway::DomainName": "AWS::ApiGateway::DomainName", "AWS::ApiGateway::BasePathMapping": "AWS::ApiGateway::BasePathMapping", + "AWS::ApiGateway::DomainNameV2": "AWS::ApiGateway::DomainNameV2", + "AWS::ApiGateway::BasePathMappingV2": "AWS::ApiGateway::BasePathMappingV2", "AWS::StepFunctions::StateMachine": "AWS::Serverless::StateMachine", "AWS::AppSync::GraphQLApi": "AWS::Serverless::GraphQLApi", } diff --git a/schema_source/sam.schema.json b/schema_source/sam.schema.json index b74f1db9f1..4a5dbf6e00 100644 --- a/schema_source/sam.schema.json +++ b/schema_source/sam.schema.json @@ -3618,7 +3618,8 @@ { "enum": [ "REGIONAL", - "EDGE" + "EDGE", + "PRIVATE" ], "type": "string" } @@ -3669,6 +3670,9 @@ ], "title": "OwnershipVerificationCertificateArn" }, + "Policy": { + "$ref": "#/definitions/PassThroughProp" + }, "Route53": { "allOf": [ { diff --git a/tests/translator/input/api_with_custom_domains_private.yaml b/tests/translator/input/api_with_custom_domains_private.yaml new file mode 100644 index 0000000000..5f6cb4709d --- /dev/null +++ b/tests/translator/input/api_with_custom_domains_private.yaml @@ -0,0 +1,62 @@ +Parameters: + DomainName: + Type: String + Default: private.example.com + Description: Custom domain name for the API + + CertificateArn: + Type: String + Default: another-api-arn + Description: ARN of the ACM certificate for the domain + + VpcEndpointId: + Type: String + Default: vpce-abcd1234efg + Description: VPC Endpoint ID for private API access + +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Domain: + DomainName: !Ref DomainName + CertificateArn: !Ref CertificateArn + EndpointConfiguration: PRIVATE + Policy: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: '*' + Action: execute-api:Invoke + Resource: execute-api:/* + - Effect: Deny + Principal: '*' + Action: execute-api:Invoke + Resource: execute-api:/* + Condition: + StringNotEquals: + aws:SourceVpce: !Ref VpcEndpointId + Auth: + ResourcePolicy: + CustomStatements: + - Effect: Allow + Principal: '*' + Action: execute-api:Invoke + Resource: execute-api:/* + - Effect: Deny + Principal: '*' + Action: execute-api:Invoke + Resource: execute-api:/* + Condition: + StringNotEquals: + aws:SourceVpce: !Ref VpcEndpointId + +Outputs: + ApiDomainName: + Description: Custom Domain Name for the API + Value: !Ref MyApi.DomainNameV2 + + ApiEndpoint: + Description: API Gateway endpoint URL + Value: !Sub https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/ diff --git a/tests/translator/output/api_with_custom_domains_private.json b/tests/translator/output/api_with_custom_domains_private.json new file mode 100644 index 0000000000..2e1e715fb3 --- /dev/null +++ b/tests/translator/output/api_with_custom_domains_private.json @@ -0,0 +1,142 @@ +{ + "Outputs": { + "ApiDomainName": { + "Description": "Custom Domain Name for the API", + "Value": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + } + }, + "ApiEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" + } + } + }, + "Parameters": { + "CertificateArn": { + "Default": "another-api-arn", + "Description": "ARN of the ACM certificate for the domain", + "Type": "String" + }, + "DomainName": { + "Default": "private.example.com", + "Description": "Custom domain name for the API", + "Type": "String" + }, + "VpcEndpointId": { + "Default": "vpce-abcd1234efg", + "Description": "VPC Endpoint ID for private API access", + "Type": "String" + } + }, + "Resources": { + "ApiGatewayDomainNameV27c603ed871": { + "Properties": { + "CertificateArn": "another-api-arn", + "DomainName": "private.example.com", + "EndpointConfiguration": { + "Types": [ + "PRIVATE" + ] + }, + "Policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::ApiGateway::DomainNameV2" + }, + "MyApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Ref": "AWS::StackName" + }, + "version": "1.0" + }, + "paths": {}, + "swagger": "2.0", + "x-amazon-apigateway-policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiBasePathMapping": { + "Properties": { + "DomainNameArn": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "Stage": { + "Ref": "MyApiprodStage" + } + }, + "Type": "AWS::ApiGateway::BasePathMappingV2" + }, + "MyApiDeployment7c3b13a843": { + "Properties": { + "Description": "RestApi deployment id: 7c3b13a843cdd653d1310c6fd7881e8fe8e49da8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApiprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment7c3b13a843" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "prod" + }, + "Type": "AWS::ApiGateway::Stage" + } + } +} diff --git a/tests/translator/output/aws-cn/api_with_custom_domains_private.json b/tests/translator/output/aws-cn/api_with_custom_domains_private.json new file mode 100644 index 0000000000..9aab6c8b98 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_custom_domains_private.json @@ -0,0 +1,150 @@ +{ + "Outputs": { + "ApiDomainName": { + "Description": "Custom Domain Name for the API", + "Value": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + } + }, + "ApiEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" + } + } + }, + "Parameters": { + "CertificateArn": { + "Default": "another-api-arn", + "Description": "ARN of the ACM certificate for the domain", + "Type": "String" + }, + "DomainName": { + "Default": "private.example.com", + "Description": "Custom domain name for the API", + "Type": "String" + }, + "VpcEndpointId": { + "Default": "vpce-abcd1234efg", + "Description": "VPC Endpoint ID for private API access", + "Type": "String" + } + }, + "Resources": { + "ApiGatewayDomainNameV27c603ed871": { + "Properties": { + "CertificateArn": "another-api-arn", + "DomainName": "private.example.com", + "EndpointConfiguration": { + "Types": [ + "PRIVATE" + ] + }, + "Policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::ApiGateway::DomainNameV2" + }, + "MyApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Ref": "AWS::StackName" + }, + "version": "1.0" + }, + "paths": {}, + "swagger": "2.0", + "x-amazon-apigateway-policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiBasePathMapping": { + "Properties": { + "DomainNameArn": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "Stage": { + "Ref": "MyApiprodStage" + } + }, + "Type": "AWS::ApiGateway::BasePathMappingV2" + }, + "MyApiDeployment7c3b13a843": { + "Properties": { + "Description": "RestApi deployment id: 7c3b13a843cdd653d1310c6fd7881e8fe8e49da8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApiprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment7c3b13a843" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "prod" + }, + "Type": "AWS::ApiGateway::Stage" + } + } +} diff --git a/tests/translator/output/aws-us-gov/api_with_custom_domains_private.json b/tests/translator/output/aws-us-gov/api_with_custom_domains_private.json new file mode 100644 index 0000000000..9aab6c8b98 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_custom_domains_private.json @@ -0,0 +1,150 @@ +{ + "Outputs": { + "ApiDomainName": { + "Description": "Custom Domain Name for the API", + "Value": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + } + }, + "ApiEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" + } + } + }, + "Parameters": { + "CertificateArn": { + "Default": "another-api-arn", + "Description": "ARN of the ACM certificate for the domain", + "Type": "String" + }, + "DomainName": { + "Default": "private.example.com", + "Description": "Custom domain name for the API", + "Type": "String" + }, + "VpcEndpointId": { + "Default": "vpce-abcd1234efg", + "Description": "VPC Endpoint ID for private API access", + "Type": "String" + } + }, + "Resources": { + "ApiGatewayDomainNameV27c603ed871": { + "Properties": { + "CertificateArn": "another-api-arn", + "DomainName": "private.example.com", + "EndpointConfiguration": { + "Types": [ + "PRIVATE" + ] + }, + "Policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::ApiGateway::DomainNameV2" + }, + "MyApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Ref": "AWS::StackName" + }, + "version": "1.0" + }, + "paths": {}, + "swagger": "2.0", + "x-amazon-apigateway-policy": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Principal": "*", + "Resource": "execute-api:/*" + }, + { + "Action": "execute-api:Invoke", + "Condition": { + "StringNotEquals": { + "aws:SourceVpce": "vpce-abcd1234efg" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": "execute-api:/*" + } + ], + "Version": "2012-10-17" + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiBasePathMapping": { + "Properties": { + "DomainNameArn": { + "Ref": "ApiGatewayDomainNameV27c603ed871" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "Stage": { + "Ref": "MyApiprodStage" + } + }, + "Type": "AWS::ApiGateway::BasePathMappingV2" + }, + "MyApiDeployment7c3b13a843": { + "Properties": { + "Description": "RestApi deployment id: 7c3b13a843cdd653d1310c6fd7881e8fe8e49da8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApiprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment7c3b13a843" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "prod" + }, + "Type": "AWS::ApiGateway::Stage" + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 802782666d..89840cae9f 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -331,6 +331,7 @@ def test_transform_feature_toggle(self, testcase, partition_with_region, mock_ge "api_with_custom_domain_route53_hosted_zone_name", "api_with_custom_domain_route53_multiple", "api_with_custom_domain_route53_multiple_intrinsic_hostedzoneid", + "api_with_custom_domains_private", "api_with_basic_custom_domain_http", "api_with_basic_custom_domain_intrinsics_http", "api_with_custom_domain_route53_http",