samcli/hook_packages/terraform/hooks/prepare/resources/apigw.py (125 lines of code) (raw):
"""
Module for API Gateway-related Terraform translation logic
"""
import logging
from typing import Dict, List, Optional
from samcli.hook_packages.terraform.hooks.prepare.exceptions import OpenAPIBodyNotSupportedException
from samcli.hook_packages.terraform.hooks.prepare.types import (
References,
ResourceProperties,
ResourceTranslationValidator,
TFResource,
)
LOG = logging.getLogger(__name__)
INVOKE_ARN_FORMAT = (
"arn:${{AWS::Partition}}:apigateway:${{AWS::Region}}:"
"lambda:path/2015-03-31/functions/${{{function_logical_id}.Arn}}/invocations"
)
INTEGRATION_PROPERTIES = [
"Uri",
"Type",
"ContentHandling",
"ConnectionType",
]
class RESTAPITranslationValidator(ResourceTranslationValidator):
def validate(self):
"""
Validation function to check if the API Gateway REST API resource can be
translated and used by AWS SAM CLI
Raises
-------
OpenAPIBodyNotSupportedException if the given api_gateway_rest_api resource contains
an OpenAPI spec with a reference to a computed value not parsable by AWS SAM CLI
"""
if _unsupported_reference_field("body", self.resource, self.config_resource):
raise OpenAPIBodyNotSupportedException(self.config_resource.full_address)
def _unsupported_reference_field(field: str, resource: Dict, config_resource: TFResource) -> bool:
"""
Check if a field in a resource is a reference to a computed value that is unknown until
apply-time. These fields are not visible to AWS SAM CLI until the Terraform application
is applied, meaning that the field isn't parsable by `sam local` commands and isn't supported
with the current hook implementation.
Parameters
----------
field: str
String representation of the field to looks for
resource: Dict
Dict containing the resource properties to look in
config_resource
The configuration resource that will contain possible references
Returns
-------
bool
True if the resource contains an field with a reference not parsable by AWS SAM CLI,
False otherwise
"""
return bool(
not (resource.get(field) or resource.get("values", {}).get(field))
and config_resource.attributes.get(field)
and isinstance(config_resource.attributes.get(field), References)
)
class ApiGatewayResourceProperties(ResourceProperties):
"""
contains the collection logic of the required properties for linking the aws_api_gateway_resource resources.
"""
def __init__(self):
super(ApiGatewayResourceProperties, self).__init__()
class ApiGatewayMethodProperties(ResourceProperties):
"""
contains the collection logic of the required properties for linking the aws_api_gateway_method resources.
"""
def __init__(self):
super(ApiGatewayMethodProperties, self).__init__()
class ApiGatewayRestApiProperties(ResourceProperties):
"""
contains the collection logic of the required properties for linking the aws_api_gateway_rest_api resources.
"""
def __init__(self):
super(ApiGatewayRestApiProperties, self).__init__()
class ApiGatewayStageProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_stage resources.
"""
def __init__(self):
super(ApiGatewayStageProperties, self).__init__()
class ApiGatewayAuthorizerProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_authorizer resources.
"""
def __init__(self):
super(ApiGatewayAuthorizerProperties, self).__init__()
class ApiGatewayV2RouteProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_v2_route resources.
"""
def __init__(self):
super(ApiGatewayV2RouteProperties, self).__init__()
class ApiGatewayV2ApiProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_apigatewayv2_api resources.
"""
def __init__(self):
super(ApiGatewayV2ApiProperties, self).__init__()
class ApiGatewayV2IntegrationProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_v2_integration resources.
"""
def __init__(self):
super(ApiGatewayV2IntegrationProperties, self).__init__()
class ApiGatewayV2AuthorizerProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_v2_authorizer resources.
"""
def __init__(self):
super(ApiGatewayV2AuthorizerProperties, self).__init__()
class ApiGatewayV2StageProperties(ResourceProperties):
"""
Contains the collection logic of the required properties for linking the aws_api_gateway_v2_stage resources.
"""
def __init__(self):
super(ApiGatewayV2StageProperties, self).__init__()
def add_integrations_to_methods(
gateway_methods_cfn: Dict[str, List], gateway_integrations_cfn: Dict[str, List]
) -> None:
"""
Iterate through all the API Gateway methods in the translated CFN dict. For each API Gateway method,
search the internal integration resources using the integrations' unique identifier to find the
one that corresponds with that API Gateway method. Once found, append the properties of the internal
integration resource to match what CFN expects, which is an 'Integration' property on the API Gateway
method resource itself.
E.g.
AwsApiGatewayMethod:
Type: AWS::ApiGateway::Method
Properties:
Integration:
Uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${function.Arn}/invocations
Type: AWS_PROXY
Parameters
----------
gateway_methods_cfn: Dict[str, List]
Dict containing API Gateway Methods to be mutated with addition integration properties
gateway_integrations_cfn: Dict[str, List]
Dict containing Internal API Gateway integrations to be appended to the CFN dict
"""
for config_address, cfn_dicts in gateway_methods_cfn.items():
for method_resource in cfn_dicts:
resource_properties = method_resource.get("Properties", {})
search_key = _gateway_method_integration_identifier(resource_properties)
integration_properties = _find_gateway_integration(search_key, gateway_integrations_cfn)
if not integration_properties:
LOG.debug("A corresponding gateway integration for the gateway method %s was not found", config_address)
continue
_create_gateway_method_integration(method_resource, integration_properties)
def add_integration_responses_to_methods(
gateway_methods_cfn: Dict[str, List], gateway_integration_responses_cfn: Dict[str, List]
) -> None:
"""
Iterate through all the API Gateway methods in the translated CFN dict. For each API Gateway method,
search the internal integration response resources using the responses' unique identifier to find the
one that corresponds with that API Gateway method. Once found, update the matched Method resource to update its
integration property to append the properties of the internal integration response resource to its
IntegrationResponses list.
E.g.
AwsApiGatewayMethod:
Type: AWS::ApiGateway::Method
Properties:
Integration:
Uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${function.Arn}/invocations
IntegrationResponses:
- ResponseParameters:
- "method.response.header.X-Some-Header": "integration.response.header.X-Some-Other-Header"
Parameters
----------
gateway_methods_cfn: Dict[str, List]
Dict containing API Gateway Methods to be mutated with addition integration properties
gateway_integration_responses_cfn: Dict[str, List]
Dict containing Internal API Gateway integration responses to be appended to the CFN dict
"""
for config_address, cfn_dicts in gateway_methods_cfn.items():
for method_resource in cfn_dicts:
method_resource_properties = method_resource.get("Properties", {})
search_key = _gateway_method_integration_identifier(method_resource_properties)
integration_response_properties = _find_gateway_integration(search_key, gateway_integration_responses_cfn)
if not integration_response_properties:
LOG.debug(
"A corresponding gateway integration response for the gateway method %s was not found",
config_address,
)
continue
_create_gateway_method_integration_response(method_resource, integration_response_properties)
def _find_gateway_integration(search_key: set, gateway_integrations_cfn: Dict[str, List]) -> Optional[dict]:
"""
Iterate through all internal API Gateway integration or integration response and search of an
integration / integration response whose unique identifier matches the given search key.
Parameters
----------
search_key: set
Set containing the unique identifier of the API Gateway integration to match
gateway_integrations_cfn: Dict[str, List]
Dict containing all Internal API Gateway integration resources to search through
Returns
-------
Properties of the internal API Gateway integration / integration response if found, otherwise returns None
"""
for _, gateway_integrations in gateway_integrations_cfn.items():
for resource in gateway_integrations:
resource_properties = resource.get("Properties", {})
integration_key = _gateway_method_integration_identifier(resource_properties)
if integration_key == search_key:
return dict(resource_properties)
return None
def _gateway_method_integration_identifier(resource_properties: dict) -> set:
"""
Given a dict containing the properties that uniquely identify an
API Gateway integration (RestApiId, ResourceId, HttpMethod)
returns a set containing these fields that be used to check for equality of integrations.
Parameters
----------
resource_properties: dict
Dict containing the resource properties that can be used to uniquely identify and API Gateway integration
Returns
-------
Returns a set comprised of unique identifiers of an API Gateway integration
"""
rest_api_id = _get_reference_from_string_or_intrinsic(resource_properties, "RestApiId")
resource_id = _get_reference_from_string_or_intrinsic(resource_properties, "ResourceId")
return {
rest_api_id,
resource_id,
resource_properties.get("HttpMethod", ""),
}
def _get_reference_from_string_or_intrinsic(resource_properties: dict, property_key: str) -> str:
"""
Check if a reference value is a constant string ARN or if it is a reference to a logical ID.
Return either the ARN or the logical ID
Parameters
----------
resource_properties: dict
Resource properties to search through
property_key: str
Property to find
Returns
-------
A string corresponding to the reference of the given field
"""
return str(
resource_properties.get(property_key, {}).get("Ref", "")
if isinstance(resource_properties.get(property_key), dict)
else resource_properties.get(property_key, "")
)
def _create_gateway_method_integration(method_resource: dict, integration_resource_properties: dict) -> None:
"""
Set the relevant resource properties defined in the integration
internal resource on the API Gateway method resource Integration field
Parameters
----------
method_resource: dict
Dict containing the AWS CFN resource for the API Gateway method resource
integration_resource_properties: dict
Dict containing the resource properties from the Internal Gateway Integration CFN resource
"""
method_resource["Properties"]["Integration"] = {}
for integration_property in INTEGRATION_PROPERTIES:
property_value = integration_resource_properties.get(integration_property, "")
if property_value:
method_resource["Properties"]["Integration"][integration_property] = property_value
def _create_gateway_method_integration_response(
method_resource: dict, integration_response_resource_properties: dict
) -> None:
"""
Set the relevant resource properties defined in the integration response internal resource on the API Gateway
method resource Integration field
Parameters
----------
method_resource: dict
Dict containing the AWS CFN resource for the API Gateway method resource
integration_response_resource_properties: dict
Dict containing the resource properties from the Internal Gateway Integration Response CFN resource
"""
integration_resource = method_resource["Properties"].get("Integration", {})
integration_responses_list = integration_resource.get("IntegrationResponses", [])
integration_responses_list.append(
{"ResponseParameters": integration_response_resource_properties.get("ResponseParameters", {})}
)
integration_resource["IntegrationResponses"] = integration_responses_list
method_resource["Properties"]["Integration"] = integration_resource