diff --git a/samcli/commands/_utils/custom_options/hook_name_option.py b/samcli/commands/_utils/custom_options/hook_name_option.py index 87888d63a1..94f89445e2 100644 --- a/samcli/commands/_utils/custom_options/hook_name_option.py +++ b/samcli/commands/_utils/custom_options/hook_name_option.py @@ -9,14 +9,7 @@ import click from samcli.cli.context import Context -from samcli.cli.global_config import GlobalConfig from samcli.commands._utils.constants import DEFAULT_BUILT_TEMPLATE_PATH -from samcli.commands._utils.experimental import ( - ExperimentalFlag, - prompt_experimental, - set_experimental, - update_experimental_context, -) from samcli.lib.hook.exceptions import InvalidHookWrapperException from samcli.lib.hook.hook_wrapper import IacHookWrapper, get_available_hook_packages_ids from samcli.lib.telemetry.event import EventName, EventTracker @@ -67,9 +60,6 @@ def handle_parse_result(self, ctx, opts, args): _validate_build_command_parameters(command_name, opts) - if not _check_experimental_flag(hook_name, command_name, opts, ctx.default_map): - return super().handle_parse_result(ctx, opts, args) - try: self._call_prepare_hook(iac_hook_wrapper, opts, ctx) except Exception as ex: @@ -149,79 +139,6 @@ def _validate_build_command_parameters(command_name, opts): ) -def _check_experimental_flag(hook_name, command_name, opts, default_map): - # check beta-feature - experimental_entry = ExperimentalFlag.IaCsSupport.get(hook_name) - beta_features = _get_customer_input_beta_features_option(default_map, experimental_entry, opts) - - # check if beta feature flag is required for a specific hook package - # The IaCs support experimental flag map will contain only the beta IaCs. In case we support the external - # hooks, we need to first know that the hook package is an external, and to handle the beta feature of it - # using different approach - if beta_features is None and experimental_entry is not None: - iac_support_message = _get_iac_support_experimental_prompt_message(hook_name, command_name) - if not prompt_experimental(experimental_entry, iac_support_message): - LOG.debug("Experimental flag is disabled and prepare hook is not run") - return False - elif not beta_features: - LOG.debug("--beta-features flag is disabled and prepare hook is not run") - return False - elif beta_features: - LOG.debug("--beta-features flag is enabled, enabling experimental flag.") - set_experimental(experimental_entry, True) - update_experimental_context() - return True - - -def _get_customer_input_beta_features_option(default_map, experimental_entry, opts): - # Get the beta-features flag value from the command parameters if provided. - beta_features = opts.get("beta_features") - if beta_features is not None: - return beta_features - - # Get the beta-features flag value from the SamConfig file if provided. - beta_features = default_map.get("beta_features") - if beta_features is not None: - return beta_features - - # Get the beta-features flag value from the environment variables. - if experimental_entry: - gc = GlobalConfig() - beta_features = gc.get_value(experimental_entry, default=None, value_type=bool, is_flag=True) - if beta_features is not None: - return beta_features - return gc.get_value(ExperimentalFlag.All, default=None, value_type=bool, is_flag=True) - - return None - - -def _get_iac_support_experimental_prompt_message(hook_name: str, command: str) -> str: - """ - return the customer prompt message for a specific hook package. - - Parameters - ---------- - hook_name: str - the hook name to determine what is the supported iac - - command: str - the current sam command - Returns - ------- - str - the customer prompt message for a specific IaC. - """ - - supported_iacs_messages = { - "terraform": ( - "Supporting Terraform applications is a beta feature.\n" - "Please confirm if you would like to proceed using AWS SAM CLI with terraform application.\n" - f"You can also enable this beta feature with 'sam {command} --beta-features'." - ) - } - return supported_iacs_messages.get(hook_name, "") - - def _read_parameter_value(param_name, opts, ctx, default_value=None): """ Read SAM CLI parameter value either from the parameters list or from the samconfig values diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 1eab7c585e..a7fb6bad9d 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -8,7 +8,6 @@ import click from samcli.cli.context import Context -from samcli.commands._utils.experimental import ExperimentalFlag, is_experimental_enabled from samcli.commands._utils.options import ( skip_prepare_infra_option, template_option_without_build, @@ -220,13 +219,6 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements """ Implementation of the ``cli`` method """ - if ( - hook_name - and ExperimentalFlag.IaCsSupport.get(hook_name) is not None - and not is_experimental_enabled(ExperimentalFlag.IaCsSupport[hook_name]) - ): - LOG.info("Terraform Support beta feature is not enabled.") - return from samcli.commands.build.build_context import BuildContext diff --git a/samcli/commands/local/invoke/cli.py b/samcli/commands/local/invoke/cli.py index f8e87f4e27..bb5de0616c 100644 --- a/samcli/commands/local/invoke/cli.py +++ b/samcli/commands/local/invoke/cli.py @@ -9,7 +9,6 @@ from samcli.cli.cli_config_file import ConfigProvider, configuration_option from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args from samcli.cli.main import common_options as cli_framework_options -from samcli.commands._utils.experimental import ExperimentalFlag, is_experimental_enabled from samcli.commands._utils.option_value_processor import process_image_options from samcli.commands._utils.options import hook_name_click_option, skip_prepare_infra_option, terraform_plan_file_option from samcli.commands.local.cli_common.options import invoke_common_options, local_common_options @@ -162,14 +161,6 @@ def do_cli( # pylint: disable=R0914 from samcli.local.docker.manager import DockerImagePullFailedException from samcli.local.lambdafn.exceptions import FunctionNotFound - if ( - hook_name - and ExperimentalFlag.IaCsSupport.get(hook_name) is not None - and not is_experimental_enabled(ExperimentalFlag.IaCsSupport.get(hook_name)) - ): - LOG.info("Terraform Support beta feature is not enabled.") - return - LOG.debug("local invoke command is called") if event: diff --git a/samcli/commands/local/start_api/cli.py b/samcli/commands/local/start_api/cli.py index 9d3247eeef..9ba18564f5 100644 --- a/samcli/commands/local/start_api/cli.py +++ b/samcli/commands/local/start_api/cli.py @@ -9,7 +9,6 @@ from samcli.cli.cli_config_file import ConfigProvider, configuration_option from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args from samcli.cli.main import common_options as cli_framework_options -from samcli.commands._utils.experimental import ExperimentalFlag, is_experimental_enabled from samcli.commands._utils.option_value_processor import process_image_options from samcli.commands._utils.options import ( generate_next_command_recommendation, @@ -186,14 +185,6 @@ def do_cli( # pylint: disable=R0914 LOG.debug("local start-api command is called") - if ( - hook_name - and ExperimentalFlag.IaCsSupport.get(hook_name) is not None - and not is_experimental_enabled(ExperimentalFlag.IaCsSupport.get(hook_name)) - ): - LOG.info("Terraform Support beta feature is not enabled.") - return - processed_invoke_images = process_image_options(invoke_image) # Pass all inputs to setup necessary context to invoke function locally. diff --git a/samcli/commands/local/start_lambda/cli.py b/samcli/commands/local/start_lambda/cli.py index 8dd78dc2f9..75184a4e58 100644 --- a/samcli/commands/local/start_lambda/cli.py +++ b/samcli/commands/local/start_lambda/cli.py @@ -9,7 +9,6 @@ from samcli.cli.cli_config_file import ConfigProvider, configuration_option from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args from samcli.cli.main import common_options as cli_framework_options -from samcli.commands._utils.experimental import ExperimentalFlag, is_experimental_enabled from samcli.commands._utils.option_value_processor import process_image_options from samcli.commands._utils.options import ( generate_next_command_recommendation, @@ -169,14 +168,6 @@ def do_cli( # pylint: disable=R0914 from samcli.lib.providers.exceptions import InvalidLayerReference from samcli.local.docker.lambda_debug_settings import DebuggingNotSupported - if ( - hook_name - and ExperimentalFlag.IaCsSupport.get(hook_name) is not None - and not is_experimental_enabled(ExperimentalFlag.IaCsSupport.get(hook_name)) - ): - LOG.info("Terraform Support beta feature is not enabled.") - return - LOG.debug("local start_lambda command is called") processed_invoke_images = process_image_options(invoke_image) diff --git a/samcli/hook_packages/terraform/hooks/prepare/constants.py b/samcli/hook_packages/terraform/hooks/prepare/constants.py index ffde46e57c..e6ea7a1fca 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/constants.py +++ b/samcli/hook_packages/terraform/hooks/prepare/constants.py @@ -23,3 +23,8 @@ TF_AWS_API_GATEWAY_INTEGRATION = "aws_api_gateway_integration" TF_AWS_API_GATEWAY_AUTHORIZER = "aws_api_gateway_authorizer" TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE = "aws_api_gateway_method_response" +TF_AWS_API_GATEWAY_V2_API = "aws_apigatewayv2_api" +TF_AWS_API_GATEWAY_V2_ROUTE = "aws_apigatewayv2_route" +TF_AWS_API_GATEWAY_V2_STAGE = "aws_apigatewayv2_stage" +TF_AWS_API_GATEWAY_V2_INTEGRATION = "aws_apigatewayv2_integration" +TF_AWS_API_GATEWAY_V2_AUTHORIZER = "aws_apigatewayv2_authorizer" diff --git a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py index f0ea430478..3a8d9c2b67 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py +++ b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py @@ -254,6 +254,126 @@ class GatewayMethodToGatewayAuthorizerLocalVariablesLinkingLimitationException( """ +class OneGatewayV2RouteToGatewayV2IntegrationLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Route linking to more than one Gateway V2 Integration + """ + + +class GatewayV2RouteToGatewayV2IntegrationLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Route linking to Gateway V2 Integration using locals. + """ + + +class OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Integration linking to more than one Lambda function + """ + + +class GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Integration linking to Lambda Function using locals. + """ + + +class OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Integration linking to more than one Gateway V2 API + """ + + +class GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Integration linking to Gateway V2 API using locals. + """ + + +class OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Route linking to more than one Gateway V2 API + """ + + +class GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException(LocalVariablesLinkingLimitationException): + """ + Exception specific for Gateway V2 Route linking to Gateway V2 API using locals. + """ + + +class OneGatewayV2AuthorizerToLambdaFunctionLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Authorizer linking to more than one Lambda Function + """ + + +class GatewayV2AuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Authorizer linking to Lambda Function using locals. + """ + + +class OneGatewayV2AuthorizerToGatewayV2ApiLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Authorizer linking to more than one Gateway V2 API + """ + + +class GatewayV2AuthorizerToGatewayV2ApiLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Authorizer linking to Gateway V2 API using locals. + """ + + +class OneGatewayV2ApiToLambdaFunctionLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 API linking to more than one Lambda Function + """ + + +class GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException(LocalVariablesLinkingLimitationException): + """ + Exception specific for Gateway V2 API linking to Lambda Function using locals. + """ + + +class OneGatewayV2StageToGatewayV2ApiLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Stage linking to more than one Gateway V2 API + """ + + +class GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException(LocalVariablesLinkingLimitationException): + """ + Exception specific for Gateway V2 Stage linking to Gateway V2 API using locals. + """ + + +class OneGatewayV2RouteToGatewayV2AuthorizerLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 Route linking to more than one Gateway V2 Authorizer + """ + + +class GatewayV2RouteToGatewayV2AuthorizerLocalVariablesLinkingLimitationException( + LocalVariablesLinkingLimitationException +): + """ + Exception specific for Gateway V2 Route linking to Gateway V2 Authorizer using locals. + """ + + class InvalidSamMetadataPropertiesException(UserException): pass diff --git a/samcli/hook_packages/terraform/hooks/prepare/property_builder.py b/samcli/hook_packages/terraform/hooks/prepare/property_builder.py index 4fe50e935a..efb88bedf3 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/property_builder.py +++ b/samcli/hook_packages/terraform/hooks/prepare/property_builder.py @@ -15,6 +15,11 @@ TF_AWS_API_GATEWAY_RESOURCE, TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_STAGE, + TF_AWS_API_GATEWAY_V2_API, + TF_AWS_API_GATEWAY_V2_AUTHORIZER, + TF_AWS_API_GATEWAY_V2_INTEGRATION, + TF_AWS_API_GATEWAY_V2_ROUTE, + TF_AWS_API_GATEWAY_V2_STAGE, TF_AWS_LAMBDA_FUNCTION, TF_AWS_LAMBDA_LAYER_VERSION, ) @@ -36,6 +41,11 @@ from samcli.lib.utils.resources import AWS_APIGATEWAY_RESOURCE as CFN_AWS_APIGATEWAY_RESOURCE from samcli.lib.utils.resources import AWS_APIGATEWAY_RESTAPI as CFN_AWS_APIGATEWAY_RESTAPI from samcli.lib.utils.resources import AWS_APIGATEWAY_STAGE as CFN_AWS_APIGATEWAY_STAGE +from samcli.lib.utils.resources import AWS_APIGATEWAY_V2_API as CFN_AWS_APIGATEWAY_V2_API +from samcli.lib.utils.resources import AWS_APIGATEWAY_V2_AUTHORIZER as CFN_AWS_APIGATEWAY_V2_AUTHORIZER +from samcli.lib.utils.resources import AWS_APIGATEWAY_V2_INTEGRATION as CFN_AWS_APIGATEWAY_V2_INTEGRATION +from samcli.lib.utils.resources import AWS_APIGATEWAY_V2_ROUTE as CFN_AWS_APIGATEWAY_V2_ROUTE +from samcli.lib.utils.resources import AWS_APIGATEWAY_V2_STAGE as CFN_AWS_APIGATEWAY_V2_STAGE from samcli.lib.utils.resources import AWS_LAMBDA_FUNCTION as CFN_AWS_LAMBDA_FUNCTION from samcli.lib.utils.resources import AWS_LAMBDA_LAYERVERSION as CFN_AWS_LAMBDA_LAYER_VERSION @@ -245,6 +255,59 @@ def _get_json_body(tf_properties: dict, resource: TFResource) -> Any: return body +def _get_cors_v2_api(tf_properties: dict, resource: TFResource) -> Optional[Dict[str, Any]]: + """ + Extract the cors properties from an aws_apigatewayv2_api since they are in a nested property + called "cors_configuration" and not in the root of the resource + + e.g. + resource "aws_apigatewayv2_api" "my_api" { + name = "my_api" + protocol_type = "HTTP" + + cors_configuration { + allow_credentials = true + allow_headers = ["Content-Type"] + allow_methods = ["OPTIONS", "POST", "GET"] + allow_origins = ["my-origin.com"] + max_age = "500" + } + } + + Parameters + ---------- + tf_properties: dict + Properties of the terraform AWS Lambda function resource + resource: TFResource + Configuration terraform resource + + Returns + ------- + Optional[Dict[str, Any]] + Optional dictionary containing the extracted cors properties with CFN property names + """ + cors = tf_properties.get("cors_configuration") + if not cors: + return None + + extractable_configs = cors[0] + cors_configuration = {} + + def _add_property(cfn_prop, tf_prop): + property_value = extractable_configs.get(tf_prop) + if property_value: + cors_configuration[cfn_prop] = property_value + + _add_property("AllowCredentials", "allow_credentials") + _add_property("AllowHeaders", "allow_headers") + _add_property("AllowMethods", "allow_methods") + _add_property("AllowOrigins", "allow_origins") + _add_property("ExposeHeaders", "expose_headers") + _add_property("MaxAge", "max_age") + + return cors_configuration + + AWS_LAMBDA_FUNCTION_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { "FunctionName": _get_property_extractor("function_name"), "Architectures": _get_property_extractor("architectures"), @@ -320,6 +383,46 @@ def _get_json_body(tf_properties: dict, resource: TFResource) -> Any: "ResponseParameters": _get_property_extractor("response_parameters"), } +AWS_API_GATEWAY_V2_API_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { + "Name": _get_property_extractor("name"), + "Body": _get_json_body, + "Target": _get_property_extractor("target"), + "ProtocolType": _get_property_extractor("protocol_type"), + "RouteKey": _get_property_extractor("route_key"), + "CorsConfiguration": _get_cors_v2_api, +} + +AWS_API_GATEWAY_V2_ROUTE_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { + "ApiId": _get_property_extractor("api_id"), + "RouteKey": _get_property_extractor("route_key"), + "Target": _get_property_extractor("target"), + "OperationName": _get_property_extractor("operation_name"), +} + +AWS_API_GATEWAY_V2_STAGE_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { + "ApiId": _get_property_extractor("api_id"), + "StageName": _get_property_extractor("name"), + "StageVariables": _get_property_extractor("stage_variables"), +} + +AWS_API_GATEWAY_V2_INTEGRATION_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { + "ApiId": _get_property_extractor("api_id"), + "IntegrationType": _get_property_extractor("integration_type"), + "IntegrationMethod": _get_property_extractor("integration_method"), + "IntegrationUri": _get_property_extractor("integration_uri"), + "PayloadFormatVersion": _get_property_extractor("payload_format_version"), +} + +AWS_API_GATEWAY_V2_AUTHORIZER_PROPERTY_BUILDER_MAPPING: PropertyBuilderMapping = { + "ApiId": _get_property_extractor("api_id"), + "AuthorizerType": _get_property_extractor("authorizer_type"), + "AuthorizerUri": _get_property_extractor("authorizer_uri"), + "Name": _get_property_extractor("name"), + "AuthorizerPayloadFormatVersion": _get_property_extractor("authorizer_payload_format_version"), + "IdentitySource": _get_property_extractor("identity_sources"), + "EnableSimpleResponses": _get_property_extractor("enable_simple_responses"), +} + RESOURCE_TRANSLATOR_MAPPING: Dict[str, ResourceTranslator] = { TF_AWS_LAMBDA_FUNCTION: ResourceTranslator(CFN_AWS_LAMBDA_FUNCTION, AWS_LAMBDA_FUNCTION_PROPERTY_BUILDER_MAPPING), TF_AWS_LAMBDA_LAYER_VERSION: ResourceTranslator( @@ -346,4 +449,19 @@ def _get_json_body(tf_properties: dict, resource: TFResource) -> Any: TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE: ResourceTranslator( INTERNAL_API_GATEWAY_INTEGRATION_RESPONSE, AWS_API_GATEWAY_INTEGRATION_RESPONSE_PROPERTY_BUILDER_MAPPING ), + TF_AWS_API_GATEWAY_V2_API: ResourceTranslator( + CFN_AWS_APIGATEWAY_V2_API, AWS_API_GATEWAY_V2_API_PROPERTY_BUILDER_MAPPING + ), + TF_AWS_API_GATEWAY_V2_ROUTE: ResourceTranslator( + CFN_AWS_APIGATEWAY_V2_ROUTE, AWS_API_GATEWAY_V2_ROUTE_PROPERTY_BUILDER_MAPPING + ), + TF_AWS_API_GATEWAY_V2_STAGE: ResourceTranslator( + CFN_AWS_APIGATEWAY_V2_STAGE, AWS_API_GATEWAY_V2_STAGE_PROPERTY_BUILDER_MAPPING + ), + TF_AWS_API_GATEWAY_V2_INTEGRATION: ResourceTranslator( + CFN_AWS_APIGATEWAY_V2_INTEGRATION, AWS_API_GATEWAY_V2_INTEGRATION_PROPERTY_BUILDER_MAPPING + ), + TF_AWS_API_GATEWAY_V2_AUTHORIZER: ResourceTranslator( + CFN_AWS_APIGATEWAY_V2_AUTHORIZER, AWS_API_GATEWAY_V2_AUTHORIZER_PROPERTY_BUILDER_MAPPING + ), } diff --git a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py index 5c9bba5a26..a7fbadd4e2 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py @@ -18,6 +18,15 @@ GatewayResourceToApiGatewayMethodLocalVariablesLinkingLimitationException, GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, GatewayResourceToParentResourceLocalVariablesLinkingLimitationException, + GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + GatewayV2AuthorizerToGatewayV2ApiLocalVariablesLinkingLimitationException, + GatewayV2AuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, + GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, + GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException, + GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, + GatewayV2RouteToGatewayV2AuthorizerLocalVariablesLinkingLimitationException, + GatewayV2RouteToGatewayV2IntegrationLocalVariablesLinkingLimitationException, + GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException, InvalidResourceLinkingException, LambdaFunctionToApiGatewayIntegrationLocalVariablesLinkingLimitationException, LocalVariablesLinkingLimitationException, @@ -29,6 +38,15 @@ OneGatewayResourceToApiGatewayMethodLinkingLimitationException, OneGatewayResourceToParentResourceLinkingLimitationException, OneGatewayResourceToRestApiLinkingLimitationException, + OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + OneGatewayV2AuthorizerToGatewayV2ApiLinkingLimitationException, + OneGatewayV2AuthorizerToLambdaFunctionLinkingLimitationException, + OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException, + OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException, + OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, + OneGatewayV2RouteToGatewayV2AuthorizerLinkingLimitationException, + OneGatewayV2RouteToGatewayV2IntegrationLinkingLimitationException, + OneGatewayV2StageToGatewayV2ApiLinkingLimitationException, OneLambdaFunctionResourceToApiGatewayIntegrationLinkingLimitationException, OneLambdaLayerLinkingLimitationException, OneResourceLinkingLimitationException, @@ -59,12 +77,33 @@ API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX = "aws_api_gateway_rest_api." API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX = "aws_api_gateway_resource." API_GATEWAY_AUTHORIZER_RESOURCE_ADDRESS_PREFIX = "aws_api_gateway_authorizer." +API_GATEWAY_V2_INTEGRATION_RESOURCE_ADDRESS_PREFIX = "aws_apigatewayv2_integration." +API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX = "aws_apigatewayv2_api." +API_GATEWAY_V2_AUTHORIZER_RESOURCE_ADDRESS_PREFIX = "aws_apigatewayv2_authorizer." TERRAFORM_LOCAL_VARIABLES_ADDRESS_PREFIX = "local." DATA_RESOURCE_ADDRESS_PREFIX = "data." LOG = logging.getLogger(__name__) +def _default_tf_destination_value_id_extractor(value: str) -> str: + """ + The default function to extract the Terraform destination resource id from the linking property value. The logic of + this function is to return the same input value. + + Parameters + ---------- + value: str + linking property value + + Returns + -------- + str: + the extracted destination resource value. + """ + return value + + @dataclass class ReferenceType: """ @@ -117,6 +156,10 @@ class ResourceLinkingPair: cfn_link_field_name: str cfn_resource_update_call_back_function: Callable[[Dict, List[ReferenceType]], None] linking_exceptions: ResourcePairExceptions + # function to extract the terraform destination value from the linking field value + tf_destination_value_extractor_from_link_field_value_function: Callable[ + [str], str + ] = _default_tf_destination_value_id_extractor class ResourceLinker: @@ -281,6 +324,12 @@ def _link_using_linking_fields(self, cfn_resource: Dict) -> None: if not isinstance(values, List): values = [values] + # loop on the linking field values and call the Logical Id extractor function to extrac the destination resource + # logical id. + values = [ + self._resource_pair.tf_destination_value_extractor_from_link_field_value_function(value) for value in values + ] + # build map between the destination linking field property values, and resources' logical ids expected_destinations_map = { expected_destination.terraform_resource_type_prefix: expected_destination.terraform_attribute_name @@ -1819,3 +1868,577 @@ def _link_gateway_method_to_gateway_authorizer( linking_exceptions=exceptions, ) ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_route_to_integration_callback( + gateway_v2_route_cfn_resource: Dict, gateway_v2_integration_resources: List[ReferenceType] +): + """ + Callback function that is used by the linking algorithm to update a CFN V2 Route Resource with + a reference to the Gateway V2 integration resource + + Parameters + ---------- + gateway_v2_route_cfn_resource: Dict + API Gateway V2 Route CFN resource + gateway_v2_integration_resources: List[ReferenceType] + List of referenced Gateway V2 Integrations either as the logical id of Integration resources + defined in the customer project, or ARN values for actual Integration defined + in customer's account. This list should always contain one element only. + """ + if len(gateway_v2_integration_resources) > 1: + raise InvalidResourceLinkingException("Could not link multiple Gateway V2 Integrations to one Gateway V2 Route") + + if not gateway_v2_integration_resources: + LOG.info( + "Unable to find any references to Gateway V2 Integrations, " + "skip linking Gateway V2 Route to Gateway V2 Integrations" + ) + return + + logical_id = gateway_v2_integration_resources[0] + if isinstance(logical_id, LogicalIdReference): + gateway_v2_route_cfn_resource["Properties"]["Target"] = { + "Fn::Join": ["/", ["integrations", {"Ref": logical_id.value}]] + } + elif not logical_id.value.startswith("integrations/"): + gateway_v2_route_cfn_resource["Properties"]["Target"] = f"integrations/{logical_id.value}" + else: + gateway_v2_route_cfn_resource["Properties"]["Target"] = logical_id.value + + +def _extract_gateway_v2_integration_id_from_route_target_value(value: str) -> str: + """ + Function to extract the Gateway V2 Integration id value from the Gateway V2 Route resource target property value. + The Route Target value should be in the format `integrations/` + + Parameters + ---------- + value: str + Gateway V2 Route target property value + + Returns + -------- + str: + The Gateway V2 integration id extracted from the route target property + """ + if value.startswith("integrations/"): + return value[len("integrations/") :] + return value + + +def _link_gateway_v2_route_to_integration( + gateway_route_config_resources: Dict[str, TFResource], + gateway_route_config_address_cfn_resources_map: Dict[str, List], + integration_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 Route resources to each Gateway V2 Integration + + Parameters + ---------- + gateway_route_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Routes + gateway_route_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Route + integration_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 integration resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2IntegrationLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2IntegrationLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_route_config_address_cfn_resources_map, + source_resource_tf_config=gateway_route_config_resources, + destination_resource_tf=integration_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_INTEGRATION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="target", + cfn_link_field_name="Target", + cfn_resource_update_call_back_function=_link_gateway_v2_route_to_integration_callback, + linking_exceptions=exceptions, + tf_destination_value_extractor_from_link_field_value_function=_extract_gateway_v2_integration_id_from_route_target_value, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_integration_to_lambda_function_callback( + gateway_v2_integration_cfn_resource: Dict, lambda_function_resources: List[ReferenceType] +): + """ + Callback function that is used by the linking algorithm to update a CFN V2 Route Resource with a reference to + the Gateway V2 integration resource + + Parameters + ---------- + gateway_v2_integration_cfn_resource: Dict + API Gateway V2 Integration CFN resource + lambda_function_resources: List[ReferenceType] + List of referenced lambda function either as the logical id of Integration resources + defined in the customer project, or the invocation ARN values for actual functions defined + in customer's account. This list should always contain one element only. + """ + if len(lambda_function_resources) > 1: + raise InvalidResourceLinkingException("Could not link multiple lambda functions to one Gateway V2 Integration") + + if not lambda_function_resources: + LOG.info( + "Unable to find any references to Lambda functions, skip linking Gateway V2 Integration to Lambda Functions" + ) + return + + logical_id = lambda_function_resources[0] + gateway_v2_integration_cfn_resource["Properties"]["IntegrationUri"] = ( + { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": [logical_id.value, "Arn"]}, + "/invocations", + ], + ] + } + if isinstance(logical_id, LogicalIdReference) + else logical_id.value + ) + + +def _link_gateway_v2_integration_to_lambda_function( + v2_gateway_integration_config_resources: Dict[str, TFResource], + v2_gateway_integration_config_address_cfn_resources_map: Dict[str, List], + lambda_functions_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 integration resources to each lambda functions + + Parameters + ---------- + v2_gateway_integration_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Integrations + v2_gateway_integration_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Integration + lambda_functions_resources: Dict[str, Dict] + Dictionary of all Terraform lambda functions resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=v2_gateway_integration_config_address_cfn_resources_map, + source_resource_tf_config=v2_gateway_integration_config_resources, + destination_resource_tf=lambda_functions_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="integration_uri", + cfn_link_field_name="IntegrationUri", + cfn_resource_update_call_back_function=_link_gateway_v2_integration_to_lambda_function_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_resource_to_api_callback( + gateway_v2_resource_cfn_resource: Dict, gateway_v2_api_resources: List[ReferenceType] +): + """ + Callback function that is used by the linking algorithm to update a CFN V2 Integration Resource with + a reference to the Gateway V2 Api resource + + Parameters + ---------- + gateway_v2_resource_cfn_resource: Dict + API Gateway V2 Integration CFN resource + gateway_v2_api_resources: List[ReferenceType] + List of referenced Gateway V2 Apis either as the logical id of Apis resources + defined in the customer project, or ARN values for actual Api defined + in customer's account. This list should always contain one element only. + """ + if len(gateway_v2_api_resources) > 1: + raise InvalidResourceLinkingException("Could not link multiple Gateway V2 Apis to one Gateway V2 resource") + + if not gateway_v2_api_resources: + LOG.info( + "Unable to find any references to Gateway V2 APIs, skip linking Gateway V2 resources to Gateway V2 APIs" + ) + return + + logical_id = gateway_v2_api_resources[0] + gateway_v2_resource_cfn_resource["Properties"]["ApiId"] = ( + {"Ref": logical_id.value} if isinstance(logical_id, LogicalIdReference) else logical_id.value + ) + + +def _link_gateway_v2_integration_to_api( + gateway_integration_config_resources: Dict[str, TFResource], + gateway_integration_config_address_cfn_resources_map: Dict[str, List], + api_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 Integration resources to each Gateway V2 Api + + Parameters + ---------- + gateway_integration_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Integrations + gateway_integration_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Integration + api_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 Api resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_integration_config_address_cfn_resources_map, + source_resource_tf_config=gateway_integration_config_resources, + destination_resource_tf=api_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=_link_gateway_v2_resource_to_api_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_route_to_api( + gateway_route_config_resources: Dict[str, TFResource], + gateway_route_config_address_cfn_resources_map: Dict[str, List], + api_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 Route resources to each Gateway V2 Api + + Parameters + ---------- + gateway_route_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Integrations + gateway_route_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Route + api_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 Api resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_route_config_address_cfn_resources_map, + source_resource_tf_config=gateway_route_config_resources, + destination_resource_tf=api_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=_link_gateway_v2_resource_to_api_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_authorizer_to_lambda_function( + authorizer_config_resources: Dict[str, TFResource], + authorizer_cfn_resources: Dict[str, List], + lamda_function_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding V2 Authorizer to each Lambda Function + + Parameters + ---------- + authorizer_config_resources: Dict[str, TFResource] + Dictionary of configuration Authorizer resources + authorizer_cfn_resources: Dict[str, List] + Dictionary containing resolved configuration address of CFN Authorizer resources + lamda_function_resources: Dict[str, Dict] + Dictionary of Terraform Lambda Function resources (not configuration resources). The dictionary's key is the + calculated logical id for each resource + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2AuthorizerToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2AuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=authorizer_cfn_resources, + source_resource_tf_config=authorizer_config_resources, + destination_resource_tf=lamda_function_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="authorizer_uri", + cfn_link_field_name="AuthorizerUri", + cfn_resource_update_call_back_function=_link_gateway_authorizer_to_lambda_function_call_back, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_authorizer_to_api( + v2_authorizer_config_resources: Dict[str, TFResource], + v2_authorizer_config_address_cfn_resources_map: Dict[str, List], + api_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 Authorizer resources to each Gateway V2 Api + + Parameters + ---------- + v2_authorizer_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway V2 Authorizers + v2_authorizer_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway V2 Authorizer + api_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 Api resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2AuthorizerToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2AuthorizerToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=v2_authorizer_config_address_cfn_resources_map, + source_resource_tf_config=v2_authorizer_config_resources, + destination_resource_tf=api_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=_link_gateway_v2_resource_to_api_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_api_to_function_callback( + gateway_v2_api_cfn_resource: Dict, referenced_function_resource_values: List[ReferenceType] +) -> None: + """ + Callback function that is used by the linking algorithm to update an Api Gateway V2 API CFN Resource with + a reference to the Lambda function resource through the AWS_PROXY integration. + + Parameters + ---------- + gateway_v2_api_cfn_resource: Dict + API Gateway V2 API CFN resource + referenced_function_resource_values: List[ReferenceType] + List of referenced Gateway Resources either as the logical id of Lambda function resource + defined in the customer project, or ARN values for actual Lambda function resource defined + in customer's account. This list should always contain one element only. + """ + if len(referenced_function_resource_values) > 1: + raise InvalidResourceLinkingException("Could not link a V2 API to more than one Lambda Function resources") + + if not referenced_function_resource_values: + LOG.info("Unable to find any references to Lambda functions, skip linking Lambda function to Gateway V2 API") + return + + logical_id = referenced_function_resource_values[0] + gateway_v2_api_cfn_resource["Properties"]["Target"] = ( + {"Fn::Sub": INVOKE_ARN_FORMAT.format(function_logical_id=logical_id.value)} + if isinstance(logical_id, LogicalIdReference) + else logical_id.value + ) + + +def _link_gateway_v2_api_to_function( + gateway_api_config_resources: Dict[str, TFResource], + gateway_api_config_address_cfn_resources_map: Dict[str, List], + lambda_function_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 API resources to each Lambda Function + + Parameters + ---------- + gateway_api_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway APIs + gateway_api_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway API + lambda_function_resources: Dict[str, Dict] + Dictionary of all Terraform Lambda Function resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + + # Only link APIs to resources if they are "Quick Create" APIs + quick_create_api_config_resources = { + config_address: tf_resource + for config_address, tf_resource in gateway_api_config_resources.items() + if "target" in tf_resource.attributes + } + + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_api_config_address_cfn_resources_map, + source_resource_tf_config=quick_create_api_config_resources, + destination_resource_tf=lambda_function_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="target", + cfn_link_field_name="Target", + cfn_resource_update_call_back_function=_link_gateway_v2_api_to_function_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_stage_to_api( + gateway_stage_config_resources: Dict[str, TFResource], + gateway_stage_config_address_cfn_resources_map: Dict[str, List], + api_resources: Dict[str, Dict], +): + """ + Iterate through all the resources and link the corresponding + Gateway V2 Stage resources to each Gateway V2 Api + + Parameters + ---------- + gateway_stage_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Stages + gateway_stage_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Stage + api_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 Api resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2StageToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_stage_config_address_cfn_resources_map, + source_resource_tf_config=gateway_stage_config_resources, + destination_resource_tf=api_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=_link_gateway_v2_resource_to_api_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_v2_route_to_authorizer_callback( + gateway_v2_route_cfn_resource: Dict, gateway_v2_authorizer_resources: List[ReferenceType] +): + """ + Callback function that is used by the linking algorithm to update a CFN V2 Integration Route with + a reference to the Gateway V2 Authorizer resource + + Parameters + ---------- + gateway_v2_route_cfn_resource: Dict + API Gateway V2 Route CFN resource + gateway_v2_authorizer_resources: List[ReferenceType] + List of referenced Gateway V2 Authorizers either as the logical id of Apis resources + defined in the customer project, or ID values for actual Authorizer defined + in customer's account. This list should always contain one element only. + """ + if len(gateway_v2_authorizer_resources) > 1: + raise InvalidResourceLinkingException("Could not link multiple Gateway V2 Authorizers to one Gateway V2 Route") + + if not gateway_v2_authorizer_resources: + LOG.info( + "Unable to find any references to Gateway V2 Authorizers, skip linking Gateway V2 Routes to Gateway V2 " + "Authorizers" + ) + return + + logical_id = gateway_v2_authorizer_resources[0] + gateway_v2_route_cfn_resource["Properties"]["AuthorizerId"] = ( + {"Ref": logical_id.value} if isinstance(logical_id, LogicalIdReference) else logical_id.value + ) + + +def _link_gateway_v2_route_to_authorizer( + gateway_route_config_resources: Dict[str, TFResource], + gateway_route_config_address_cfn_resources_map: Dict[str, List], + authorizer_resources: Dict[str, Dict], +): + """ + Iterate through all the resources and link the corresponding + Gateway V2 Route resources to each Gateway V2 Authorizer + + Parameters + ---------- + gateway_route_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway Routes + gateway_route_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Route + authorizer_resources: Dict[str, Dict] + Dictionary of all Terraform Gateway V2 Authorizer resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2AuthorizerLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2AuthorizerLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_route_config_address_cfn_resources_map, + source_resource_tf_config=gateway_route_config_resources, + destination_resource_tf=authorizer_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="authorizer_id", + cfn_link_field_name="AuthorizerId", + cfn_resource_update_call_back_function=_link_gateway_v2_route_to_authorizer_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() diff --git a/samcli/hook_packages/terraform/hooks/prepare/resources/apigw.py b/samcli/hook_packages/terraform/hooks/prepare/resources/apigw.py index 356c744e12..cef5e730b6 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/apigw.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/apigw.py @@ -117,6 +117,51 @@ 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: diff --git a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py index 7d9d10f574..128ca34340 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py @@ -8,6 +8,11 @@ TF_AWS_API_GATEWAY_RESOURCE, TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_STAGE, + TF_AWS_API_GATEWAY_V2_API, + TF_AWS_API_GATEWAY_V2_AUTHORIZER, + TF_AWS_API_GATEWAY_V2_INTEGRATION, + TF_AWS_API_GATEWAY_V2_ROUTE, + TF_AWS_API_GATEWAY_V2_STAGE, TF_AWS_LAMBDA_FUNCTION, TF_AWS_LAMBDA_LAYER_VERSION, ) @@ -25,6 +30,15 @@ _link_gateway_resources_to_gateway_rest_apis, _link_gateway_resources_to_parents, _link_gateway_stage_to_rest_api, + _link_gateway_v2_api_to_function, + _link_gateway_v2_authorizer_to_api, + _link_gateway_v2_authorizer_to_lambda_function, + _link_gateway_v2_integration_to_api, + _link_gateway_v2_integration_to_lambda_function, + _link_gateway_v2_route_to_api, + _link_gateway_v2_route_to_authorizer, + _link_gateway_v2_route_to_integration, + _link_gateway_v2_stage_to_api, _link_lambda_functions_to_layers, ) from samcli.hook_packages.terraform.hooks.prepare.types import ( @@ -79,6 +93,51 @@ dest=TF_AWS_API_GATEWAY_AUTHORIZER, linking_func=_link_gateway_method_to_gateway_authorizer, ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_ROUTE, + dest=TF_AWS_API_GATEWAY_V2_INTEGRATION, + linking_func=_link_gateway_v2_route_to_integration, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_INTEGRATION, + dest=TF_AWS_LAMBDA_FUNCTION, + linking_func=_link_gateway_v2_integration_to_lambda_function, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_INTEGRATION, + dest=TF_AWS_API_GATEWAY_V2_API, + linking_func=_link_gateway_v2_integration_to_api, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_ROUTE, + dest=TF_AWS_API_GATEWAY_V2_API, + linking_func=_link_gateway_v2_route_to_api, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_AUTHORIZER, + dest=TF_AWS_LAMBDA_FUNCTION, + linking_func=_link_gateway_v2_authorizer_to_lambda_function, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_AUTHORIZER, + dest=TF_AWS_API_GATEWAY_V2_API, + linking_func=_link_gateway_v2_authorizer_to_api, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_API, + dest=TF_AWS_LAMBDA_FUNCTION, + linking_func=_link_gateway_v2_api_to_function, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_STAGE, + dest=TF_AWS_API_GATEWAY_V2_API, + linking_func=_link_gateway_v2_stage_to_api, + ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_ROUTE, + dest=TF_AWS_API_GATEWAY_V2_AUTHORIZER, + linking_func=_link_gateway_v2_route_to_authorizer, + ), ] MULTIPLE_DESTINATIONS_RESOURCE_LINKS: List[LinkingMultipleDestinationsOptionsCaller] = [ diff --git a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py index f190eb43d2..76931dd22b 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py @@ -9,6 +9,11 @@ TF_AWS_API_GATEWAY_RESOURCE, TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_STAGE, + TF_AWS_API_GATEWAY_V2_API, + TF_AWS_API_GATEWAY_V2_AUTHORIZER, + TF_AWS_API_GATEWAY_V2_INTEGRATION, + TF_AWS_API_GATEWAY_V2_ROUTE, + TF_AWS_API_GATEWAY_V2_STAGE, TF_AWS_LAMBDA_FUNCTION, TF_AWS_LAMBDA_LAYER_VERSION, ) @@ -18,6 +23,11 @@ ApiGatewayResourceProperties, ApiGatewayRestApiProperties, ApiGatewayStageProperties, + ApiGatewayV2ApiProperties, + ApiGatewayV2AuthorizerProperties, + ApiGatewayV2IntegrationProperties, + ApiGatewayV2RouteProperties, + ApiGatewayV2StageProperties, ) from samcli.hook_packages.terraform.hooks.prepare.resources.internal import ( InternalApiGatewayIntegrationProperties, @@ -49,4 +59,9 @@ def get_resource_property_mapping() -> Dict[str, ResourceProperties]: TF_AWS_API_GATEWAY_INTEGRATION: InternalApiGatewayIntegrationProperties(), TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE: InternalApiGatewayIntegrationResponseProperties(), TF_AWS_API_GATEWAY_AUTHORIZER: ApiGatewayAuthorizerProperties(), + TF_AWS_API_GATEWAY_V2_ROUTE: ApiGatewayV2RouteProperties(), + TF_AWS_API_GATEWAY_V2_INTEGRATION: ApiGatewayV2IntegrationProperties(), + TF_AWS_API_GATEWAY_V2_API: ApiGatewayV2ApiProperties(), + TF_AWS_API_GATEWAY_V2_AUTHORIZER: ApiGatewayV2AuthorizerProperties(), + TF_AWS_API_GATEWAY_V2_STAGE: ApiGatewayV2StageProperties(), } diff --git a/samcli/hook_packages/terraform/hooks/prepare/translate.py b/samcli/hook_packages/terraform/hooks/prepare/translate.py index 2d8639de4f..bd32102eb2 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/translate.py +++ b/samcli/hook_packages/terraform/hooks/prepare/translate.py @@ -15,6 +15,7 @@ TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, TF_AWS_API_GATEWAY_METHOD, TF_AWS_API_GATEWAY_REST_API, + TF_AWS_API_GATEWAY_V2_API, ) from samcli.hook_packages.terraform.hooks.prepare.enrich import enrich_resources_and_generate_makefile from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( @@ -68,6 +69,7 @@ TRANSLATION_VALIDATORS: Dict[str, Type[ResourceTranslationValidator]] = { TF_AWS_API_GATEWAY_REST_API: RESTAPITranslationValidator, + TF_AWS_API_GATEWAY_V2_API: RESTAPITranslationValidator, } diff --git a/tests/integration/buildcmd/test_build_terraform_applications.py b/tests/integration/buildcmd/test_build_terraform_applications.py index 8a0044d4e7..983571b184 100644 --- a/tests/integration/buildcmd/test_build_terraform_applications.py +++ b/tests/integration/buildcmd/test_build_terraform_applications.py @@ -15,7 +15,6 @@ from parameterized import parameterized, parameterized_class -from samcli.lib.utils.colors import Colored from tests.integration.buildcmd.build_integ_base import BuildIntegBase from tests.testing_utils import CI_OVERRIDE, IS_WINDOWS, RUN_BY_CANARY @@ -76,7 +75,6 @@ def _verify_invoke_built_function(self, function_logical_id, overrides, expected "--no-event", "--hook-name", "terraform", - "--beta-features", ] if overrides: @@ -243,7 +241,6 @@ def setUpClass(cls): @parameterized.expand(functions) def test_build_and_invoke_lambda_functions(self, function_identifier, expected_output, should_override_code): command_list_parameters = { - "beta_features": True, "hook_name": "terraform", "function_identifier": function_identifier, } @@ -260,18 +257,6 @@ def test_build_and_invoke_lambda_functions(self, function_identifier, expected_o if should_override_code: environment_variables["TF_VAR_HELLO_FUNCTION_SRC_CODE"] = "./artifacts/HelloWorldFunction2" stdout, stderr, return_code = self.run_command(build_cmd_list, env=environment_variables) - terraform_beta_feature_prompted_text = ( - f"Supporting Terraform applications is a beta feature.{os.linesep}" - f"Please confirm if you would like to proceed using AWS SAM CLI with terraform application.{os.linesep}" - "You can also enable this beta feature with 'sam build --beta-features'." - ) - experimental_warning = ( - f"{os.linesep}Experimental features are enabled for this session.{os.linesep}" - f"Visit the docs page to learn more about the AWS Beta terms " - f"https://aws.amazon.com/service-terms/.{os.linesep}" - ) - self.assertNotRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertIn(Colored().yellow(experimental_warning), stderr.decode("utf-8")) LOG.info("sam build stdout: %s", stdout.decode("utf-8")) LOG.info("sam build stderr: %s", stderr.decode("utf-8")) self.assertEqual(return_code, 0) @@ -374,7 +359,6 @@ def setUpClass(cls): @parameterized.expand(functions) def test_build_and_invoke_lambda_functions(self, function_identifier, expected_output, should_override_code): command_list_parameters = { - "beta_features": True, "hook_name": "terraform", "function_identifier": function_identifier, } diff --git a/tests/integration/buildcmd/test_build_terraform_applications_other_cases.py b/tests/integration/buildcmd/test_build_terraform_applications_other_cases.py index ea57300a2f..c1efc9f7ef 100644 --- a/tests/integration/buildcmd/test_build_terraform_applications_other_cases.py +++ b/tests/integration/buildcmd/test_build_terraform_applications_other_cases.py @@ -6,7 +6,6 @@ from parameterized import parameterized, parameterized_class -from samcli.lib.utils.colors import Colored from tests.integration.buildcmd.test_build_terraform_applications import ( BuildTerraformApplicationIntegBase, BuildTerraformApplicationS3BackendIntegBase, @@ -44,7 +43,7 @@ def test_invalid_hook_name(self): self.assertNotEqual(return_code, 0) def test_exit_failed_use_container_no_build_image_hooks(self): - cmdlist = self.get_command_list(beta_features=True, hook_name="terraform", use_container=True) + cmdlist = self.get_command_list(hook_name="terraform", use_container=True) _, stderr, return_code = self.run_command(cmdlist) process_stderr = stderr.strip() self.assertRegex( @@ -54,7 +53,7 @@ def test_exit_failed_use_container_no_build_image_hooks(self): self.assertNotEqual(return_code, 0) def test_exit_failed_project_root_dir_no_hooks(self): - cmdlist = self.get_command_list(beta_features=True, project_root_dir="/path") + cmdlist = self.get_command_list(project_root_dir="/path") _, stderr, return_code = self.run_command(cmdlist) process_stderr = stderr.strip() self.assertRegex( @@ -64,7 +63,7 @@ def test_exit_failed_project_root_dir_no_hooks(self): self.assertNotEqual(return_code, 0) def test_exit_failed_project_root_dir_not_parent_of_current_directory(self): - cmdlist = self.get_command_list(beta_features=True, hook_name="terraform", project_root_dir="/path") + cmdlist = self.get_command_list(hook_name="terraform", project_root_dir="/path") _, stderr, return_code = self.run_command(cmdlist) process_stderr = stderr.strip() self.assertRegex( @@ -75,7 +74,7 @@ def test_exit_failed_project_root_dir_not_parent_of_current_directory(self): self.assertNotEqual(return_code, 0) def test_exit_failed_use_container_short_format_no_build_image_hooks(self): - cmdlist = self.get_command_list(beta_features=True, hook_name="terraform") + cmdlist = self.get_command_list(hook_name="terraform") cmdlist += ["-u"] _, stderr, return_code = self.run_command(cmdlist) process_stderr = stderr.strip() @@ -85,53 +84,6 @@ def test_exit_failed_use_container_short_format_no_build_image_hooks(self): ) self.assertNotEqual(return_code, 0) - def test_exit_success_no_beta_feature_flags_hooks(self): - cmdlist = self.get_command_list(beta_features=None, hook_name="terraform") - stdout, stderr, return_code = self.run_command(cmdlist, input=b"N\n\n") - terraform_beta_feature_prompted_text = ( - f"Supporting Terraform applications is a beta feature.{os.linesep}" - f"Please confirm if you would like to proceed using AWS SAM CLI with terraform application.{os.linesep}" - "You can also enable this beta feature with 'sam build --beta-features'." - ) - self.assertRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertEqual(return_code, 0) - self.assertRegex(stderr.strip().decode("utf-8"), "Terraform Support beta feature is not enabled.") - - def test_exit_success_no_beta_features_flags_supplied_hooks(self): - cmdlist = self.get_command_list(beta_features=False, hook_name="terraform") - _, stderr, return_code = self.run_command(cmdlist) - self.assertEqual(return_code, 0) - self.assertRegex(stderr.strip().decode("utf-8"), "Terraform Support beta feature is not enabled.") - - def test_build_terraform_with_no_beta_feature_option_in_samconfig_toml(self): - samconfig_toml_path = Path(self.working_dir).joinpath("samconfig.toml") - samconfig_lines = [ - bytes("version = 0.1" + os.linesep, "utf-8"), - bytes("[default.global.parameters]" + os.linesep, "utf-8"), - bytes("beta_features = false" + os.linesep, "utf-8"), - ] - with open(samconfig_toml_path, "wb") as file: - file.writelines(samconfig_lines) - - cmdlist = self.get_command_list(hook_name="terraform") - _, stderr, return_code = self.run_command(cmdlist) - self.assertEqual(return_code, 0) - self.assertRegex(stderr.strip().decode("utf-8"), "Terraform Support beta feature is not enabled.") - # delete the samconfig file - try: - os.remove(samconfig_toml_path) - except FileNotFoundError: - pass - - def test_build_terraform_with_no_beta_feature_option_as_environment_variable(self): - environment_variables = os.environ.copy() - environment_variables["SAM_CLI_BETA_TERRAFORM_SUPPORT"] = "False" - - build_command_list = self.get_command_list(hook_name="terraform") - _, stderr, return_code = self.run_command(build_command_list, env=environment_variables) - self.assertEqual(return_code, 0) - self.assertRegex(stderr.strip().decode("utf-8"), "Terraform Support beta feature is not enabled.") - @skipIf( (not RUN_BY_CANARY and not CI_OVERRIDE), @@ -142,9 +94,7 @@ class TestInvalidTerraformApplicationThatReferToS3BucketNotCreatedYet(BuildTerra def test_invoke_function(self): function_identifier = "aws_lambda_function.function" - build_cmd_list = self.get_command_list( - beta_features=True, hook_name="terraform", function_identifier=function_identifier - ) + build_cmd_list = self.get_command_list(hook_name="terraform", function_identifier=function_identifier) LOG.info("command list: %s", build_cmd_list) environment_variables = os.environ.copy() @@ -175,7 +125,6 @@ class TestInvalidBuildTerraformApplicationsWithZipBasedLambdaFunctionAndS3Backen def test_build_no_s3_config(self): command_list_parameters = { - "beta_features": True, "hook_name": "terraform", } build_cmd_list = self.get_command_list(**command_list_parameters) @@ -206,9 +155,7 @@ class TestBuildTerraformApplicationsWithImageBasedLambdaFunctionAndLocalBackend( @parameterized.expand(functions) def test_build_and_invoke_lambda_functions(self, function_identifier): - build_cmd_list = self.get_command_list( - beta_features=True, hook_name="terraform", function_identifier=function_identifier - ) + build_cmd_list = self.get_command_list(hook_name="terraform", function_identifier=function_identifier) LOG.info("command list: %s", build_cmd_list) _, stderr, return_code = self.run_command(build_cmd_list) LOG.info(stderr) @@ -248,9 +195,7 @@ class TestBuildTerraformApplicationsWithImageBasedLambdaFunctionAndS3Backend( @parameterized.expand(functions) def test_build_and_invoke_lambda_functions(self, function_identifier): - build_cmd_list = self.get_command_list( - beta_features=True, hook_name="terraform", function_identifier=function_identifier - ) + build_cmd_list = self.get_command_list(hook_name="terraform", function_identifier=function_identifier) LOG.info("command list: %s", build_cmd_list) _, stderr, return_code = self.run_command(build_cmd_list) LOG.info(stderr) @@ -312,7 +257,7 @@ def test_unsupported_cases(self, app, expected_error_message): self.terraform_application_path = Path(self.terraform_application_path) / app shutil.copytree(Path(self.terraform_application_path), Path(self.working_dir)) - build_cmd_list = self.get_command_list(beta_features=True, hook_name="terraform") + build_cmd_list = self.get_command_list(hook_name="terraform") LOG.info("command list: %s", build_cmd_list) _, stderr, return_code = self.run_command(build_cmd_list) LOG.info(stderr) @@ -370,7 +315,7 @@ def tearDown(self): self.assertEqual(return_code, 0) def test_unsupported_cases_runs_after_apply(self): - build_cmd_list = self.get_command_list(beta_features=True, hook_name="terraform") + build_cmd_list = self.get_command_list(hook_name="terraform") LOG.info("command list: %s", build_cmd_list) _, _, return_code = self.run_command(build_cmd_list) self.assertEqual(return_code, 0) @@ -390,9 +335,7 @@ class TestBuildGoFunctionAndKeepPermissions(BuildTerraformApplicationIntegBase): def test_invoke_function(self): function_identifier = "hello-world-function" - build_cmd_list = self.get_command_list( - beta_features=True, hook_name="terraform", function_identifier=function_identifier - ) + build_cmd_list = self.get_command_list(hook_name="terraform", function_identifier=function_identifier) LOG.info("command list: %s", build_cmd_list) environment_variables = os.environ.copy() @@ -461,7 +404,6 @@ def tearDown(self): @parameterized.expand(functions) def test_build_and_invoke_lambda_functions(self, function_identifier, expected_output): command_list_parameters = { - "beta_features": True, "hook_name": "terraform", "function_identifier": function_identifier, "project_root_dir": "./..", @@ -472,18 +414,6 @@ def test_build_and_invoke_lambda_functions(self, function_identifier, expected_o build_cmd_list = self.get_command_list(**command_list_parameters) LOG.info("command list: %s", build_cmd_list) stdout, stderr, return_code = self.run_command(build_cmd_list) - terraform_beta_feature_prompted_text = ( - f"Supporting Terraform applications is a beta feature.{os.linesep}" - f"Please confirm if you would like to proceed using AWS SAM CLI with terraform application.{os.linesep}" - "You can also enable this beta feature with 'sam build --beta-features'." - ) - experimental_warning = ( - f"{os.linesep}Experimental features are enabled for this session.{os.linesep}" - f"Visit the docs page to learn more about the AWS Beta terms " - f"https://aws.amazon.com/service-terms/.{os.linesep}" - ) - self.assertNotRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertIn(Colored().yellow(experimental_warning), stderr.decode("utf-8")) LOG.info("sam build stdout: %s", stdout.decode("utf-8")) LOG.info("sam build stderr: %s", stderr.decode("utf-8")) self.assertEqual(return_code, 0) @@ -527,18 +457,6 @@ def test_build_and_invoke_lambda_functions(self, function_identifier, expected_o build_cmd_list = self.get_command_list(**command_list_parameters) LOG.info("command list: %s", build_cmd_list) stdout, stderr, return_code = self.run_command(build_cmd_list) - terraform_beta_feature_prompted_text = ( - f"Supporting Terraform applications is a beta feature.{os.linesep}" - f"Please confirm if you would like to proceed using AWS SAM CLI with terraform application.{os.linesep}" - "You can also enable this beta feature with 'sam build --beta-features'." - ) - experimental_warning = ( - f"{os.linesep}Experimental features are enabled for this session.{os.linesep}" - f"Visit the docs page to learn more about the AWS Beta terms " - f"https://aws.amazon.com/service-terms/.{os.linesep}" - ) - self.assertNotRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertIn(Colored().yellow(experimental_warning), stderr.decode("utf-8")) LOG.info("sam build stdout: %s", stdout.decode("utf-8")) LOG.info("sam build stderr: %s", stderr.decode("utf-8")) self.assertEqual(return_code, 0) diff --git a/tests/integration/local/common_utils.py b/tests/integration/local/common_utils.py index 9ed98ed361..0dbb23aa7c 100644 --- a/tests/integration/local/common_utils.py +++ b/tests/integration/local/common_utils.py @@ -28,11 +28,7 @@ def wait_for_local_process(process, port, collect_output=False) -> str: if "Address already in use" in line_as_str: LOG.info(f"Attempted to start port on {port} but it is already in use, restarting on a new port.") raise InvalidAddressException() - if ( - "Press CTRL+C to quit" in line_as_str - or "Terraform Support beta feature is not enabled." in line_as_str - or "Error: " in line_as_str - ): + if "Press CTRL+C to quit" in line_as_str or "Error: " in line_as_str: break return output diff --git a/tests/integration/local/invoke/test_invoke_terraform_applications.py b/tests/integration/local/invoke/test_invoke_terraform_applications.py index 792025f65d..e810696118 100644 --- a/tests/integration/local/invoke/test_invoke_terraform_applications.py +++ b/tests/integration/local/invoke/test_invoke_terraform_applications.py @@ -15,11 +15,8 @@ from docker.errors import APIError from parameterized import parameterized, parameterized_class -from samcli.commands._utils.experimental import EXPERIMENTAL_WARNING -from samcli.lib.utils.colors import Colored from tests.integration.local.invoke.invoke_integ_base import InvokeIntegBase, TIMEOUT from tests.integration.local.invoke.layer_utils import LayerUtils -from tests.integration.local.start_lambda.start_lambda_api_integ_base import StartLambdaIntegBaseClass from tests.testing_utils import CI_OVERRIDE, IS_WINDOWS, RUNNING_ON_CI, RUN_BY_CANARY LOG = logging.getLogger(__name__) @@ -76,7 +73,7 @@ def tearDown(self) -> None: @pytest.mark.flaky(reruns=3) def test_invoke_function(self, function_name): local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke=function_name, hook_name="terraform", beta_features=True + function_to_invoke=function_name, hook_name="terraform" ) stdout, _, return_code = self.run_command(local_invoke_command_list) @@ -97,7 +94,6 @@ def test_invoke_function_custom_plan(self, function_name): local_invoke_command_list = InvokeIntegBase.get_command_list( function_to_invoke=function_name, hook_name="terraform", - beta_features=True, terraform_plan_file="custom-plan.json", ) stdout, _, return_code = self.run_command(local_invoke_command_list) @@ -110,7 +106,7 @@ def test_invoke_function_custom_plan(self, function_name): self.assertEqual(response, expected_response) def test_exit_failed_project_root_dir_no_hooks_custom_plan_file(self): - cmdlist = self.get_command_list(beta_features=True, terraform_plan_file="/path", function_to_invoke="") + cmdlist = self.get_command_list(terraform_plan_file="/path", function_to_invoke="") _, stderr, return_code = self.run_command(cmdlist) process_stderr = stderr.strip() self.assertRegex( @@ -119,137 +115,6 @@ def test_exit_failed_project_root_dir_no_hooks_custom_plan_file(self): ) self.assertNotEqual(return_code, 0) - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @parameterized.expand(functions) - @pytest.mark.flaky(reruns=3) - def test_invoke_terraform_with_beta_feature_option_in_samconfig_toml(self, function_name): - samconfig_toml_path = Path(self.terraform_application_path).joinpath("samconfig.toml") - samconfig_lines = [ - bytes("version = 0.1" + os.linesep, "utf-8"), - bytes("[default.global.parameters]" + os.linesep, "utf-8"), - bytes("beta_features = true" + os.linesep, "utf-8"), - ] - with open(samconfig_toml_path, "wb") as file: - file.writelines(samconfig_lines) - - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke=function_name, hook_name="terraform" - ) - stdout, _, return_code = self.run_command(local_invoke_command_list) - - # Get the response without the sam-cli prompts that proceed it - response = json.loads(stdout.decode("utf-8").split("\n")[-1]) - expected_response = json.loads('{"statusCode":200,"body":"{\\"message\\": \\"hello world\\"}"}') - - self.assertEqual(return_code, 0) - self.assertEqual(response, expected_response) - # delete the samconfig file - try: - os.remove(samconfig_toml_path) - except FileNotFoundError: - pass - - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @parameterized.expand(functions) - @pytest.mark.flaky(reruns=3) - def test_invoke_terraform_with_beta_feature_option_as_environment_variable(self, function_name): - environment_variables = os.environ.copy() - environment_variables["SAM_CLI_BETA_TERRAFORM_SUPPORT"] = "1" - - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke=function_name, hook_name="terraform" - ) - stdout, _, return_code = self.run_command(local_invoke_command_list, env=environment_variables) - - # Get the response without the sam-cli prompts that proceed it - response = json.loads(stdout.decode("utf-8").split("\n")[-1]) - expected_response = json.loads('{"statusCode":200,"body":"{\\"message\\": \\"hello world\\"}"}') - - self.assertEqual(return_code, 0) - self.assertEqual(response, expected_response) - - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @pytest.mark.flaky(reruns=3) - def test_invoke_function_get_experimental_prompted(self): - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke="s3_lambda_function", hook_name="terraform" - ) - stdout, stderr, return_code = self.run_command(local_invoke_command_list, input=b"Y\n\n") - - terraform_beta_feature_prompted_text = ( - "Supporting Terraform applications is a beta feature.\n" - "Please confirm if you would like to proceed using AWS SAM CLI with terraform application.\n" - "You can also enable this beta feature with 'sam local invoke --beta-features'." - ) - self.assertRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertRegex(stderr.decode("utf-8"), EXPERIMENTAL_WARNING) - - response = json.loads(stdout.decode("utf-8").split("\n")[2][85:].strip()) - expected_response = json.loads('{"statusCode":200,"body":"{\\"message\\": \\"hello world\\"}"}') - - self.assertEqual(return_code, 0) - self.assertEqual(response, expected_response) - - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @pytest.mark.flaky(reruns=3) - def test_invoke_function_with_beta_feature_expect_warning_message(self): - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke="s3_lambda_function", hook_name="terraform", beta_features=True - ) - stdout, stderr, return_code = self.run_command(local_invoke_command_list) - - terraform_beta_feature_prompted_text = ( - "Supporting Terraform applications is a beta feature.\n" - "Please confirm if you would like to proceed using AWS SAM CLI with terraform application.\n" - "You can also enable this beta feature with 'sam local invoke --beta-features'." - ) - self.assertNotRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - self.assertTrue(Colored().yellow(EXPERIMENTAL_WARNING) in stderr.decode("utf-8")) - - response = json.loads(stdout.decode("utf-8").split("\n")[-1]) - expected_response = json.loads('{"statusCode":200,"body":"{\\"message\\": \\"hello world\\"}"}') - - self.assertEqual(return_code, 0) - self.assertEqual(response, expected_response) - - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @pytest.mark.flaky(reruns=3) - def test_invoke_function_get_experimental_prompted_input_no(self): - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke="s3_lambda_function", hook_name="terraform" - ) - stdout, stderr, return_code = self.run_command(local_invoke_command_list, input=b"N\n\n") - - terraform_beta_feature_prompted_text = ( - "Supporting Terraform applications is a beta feature.\n" - "Please confirm if you would like to proceed using AWS SAM CLI with terraform application.\n" - "You can also enable this beta feature with 'sam local invoke --beta-features'." - ) - self.assertRegex(stdout.decode("utf-8"), terraform_beta_feature_prompted_text) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - - self.assertEqual(return_code, 0) - def test_invalid_hook_name(self): local_invoke_command_list = InvokeIntegBase.get_command_list("func", hook_name="tf") _, stderr, return_code = self.run_command(local_invoke_command_list) @@ -261,58 +126,6 @@ def test_invalid_hook_name(self): ) self.assertNotEqual(return_code, 0) - def test_invoke_terraform_with_no_beta_feature_option(self): - local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke="func", hook_name="terraform", beta_features=False - ) - _, stderr, return_code = self.run_command(local_invoke_command_list) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - - def test_invoke_terraform_with_no_beta_feature_option_in_samconfig_toml(self): - samconfig_toml_path = Path(self.terraform_application_path).joinpath("samconfig.toml") - samconfig_lines = [ - bytes("version = 0.1" + os.linesep, "utf-8"), - bytes("[default.global.parameters]" + os.linesep, "utf-8"), - bytes("beta_features = false" + os.linesep, "utf-8"), - ] - with open(samconfig_toml_path, "wb") as file: - file.writelines(samconfig_lines) - - local_invoke_command_list = InvokeIntegBase.get_command_list(function_to_invoke="func", hook_name="terraform") - _, stderr, return_code = self.run_command(local_invoke_command_list) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - # delete the samconfig file - try: - os.remove(samconfig_toml_path) - except FileNotFoundError: - pass - - def test_invoke_terraform_with_no_beta_feature_option_as_environment_variable(self): - environment_variables = os.environ.copy() - environment_variables["SAM_CLI_BETA_TERRAFORM_SUPPORT"] = "False" - - local_invoke_command_list = InvokeIntegBase.get_command_list(function_to_invoke="func", hook_name="terraform") - _, stderr, return_code = self.run_command(local_invoke_command_list, env=environment_variables) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - def test_invalid_coexist_parameters(self): local_invoke_command_list = InvokeIntegBase.get_command_list( "func", hook_name="terraform", template_path="template.yaml" @@ -474,7 +287,7 @@ def tearDown(self) -> None: @pytest.mark.flaky(reruns=3) def test_invoke_function(self, function_name, expected_output): local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke=function_name, hook_name="terraform", beta_features=True + function_to_invoke=function_name, hook_name="terraform" ) stdout, _, return_code = self.run_command(local_invoke_command_list, env=self._add_tf_project_variables()) @@ -506,7 +319,7 @@ def tearDown(self) -> None: def test_invoke_function(self): function_name = "aws_lambda_function.function" local_invoke_command_list = InvokeIntegBase.get_command_list( - function_to_invoke=function_name, hook_name="terraform", beta_features=True + function_to_invoke=function_name, hook_name="terraform" ) _, stderr, return_code = self.run_command(local_invoke_command_list) process_stderr = stderr.strip() @@ -566,7 +379,6 @@ def test_invoke_image_function(self, function_name): function_to_invoke=function_name, hook_name="terraform", event_path=self.event_path, - beta_features=True, ) stdout, _, return_code = self.run_command(local_invoke_command_list) diff --git a/tests/integration/local/start_api/test_start_api_with_terraform_application.py b/tests/integration/local/start_api/test_start_api_with_terraform_application.py index 68725170f1..74ce8743a1 100644 --- a/tests/integration/local/start_api/test_start_api_with_terraform_application.py +++ b/tests/integration/local/start_api/test_start_api_with_terraform_application.py @@ -27,7 +27,7 @@ def setUpClass(cls): command = get_sam_command() cls.template_path = "" cls.build_before_invoke = False - cls.command_list = [command, "local", "start-api", "--hook-name", "terraform", "--beta-features"] + cls.command_list = [command, "local", "start-api", "--hook-name", "terraform"] if cls.terraform_plan_file: cls.command_list += ["--terraform-plan-file", cls.terraform_plan_file] cls.test_data_path = Path(cls.get_integ_dir()) / "testdata" / "start_api" @@ -97,6 +97,7 @@ def tearDownClass(cls) -> None: not CI_OVERRIDE, "Skip Terraform test cases unless running in CI", ) +@pytest.mark.flaky(reruns=3) @parameterized_class( [ { @@ -107,9 +108,16 @@ def tearDownClass(cls) -> None: "terraform_application": "terraform-v1-api-simple", "testing_urls": ["hello"], }, + { + "terraform_application": "terraform-v2-api-simple", + "testing_urls": ["hello"], + }, + { + "terraform_application": "terraform-v2-api-quick-create", + "testing_urls": ["hello"], + }, ] ) -@pytest.mark.flaky(reruns=3) class TestStartApiTerraformApplication(TerraformStartApiIntegrationBase): def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) @@ -164,6 +172,21 @@ def test_successful_request(self): "expected_error_message": "Error: AWS SAM CLI could not process a Terraform project that uses local " "variables to define linked resources.", }, + { + "terraform_application": "terraform-v2-api-simple-multi-resource-link", + "expected_error_message": "Error: AWS SAM CLI could not process a Terraform project that contains a source " + "resource that is linked to more than one destination resource.", + }, + { + "terraform_application": "terraform-v2-api-simple-local-resource-link", + "expected_error_message": "Error: AWS SAM CLI could not process a Terraform project that uses local " + "variables to define linked resources.", + }, + { + "terraform_application": "terraform-v2-openapi", + "expected_error_message": "Error: AWS SAM CLI is unable to process a Terraform project that uses an OpenAPI" + " specification to define the API Gateway resource.", + }, ] ) class TestStartApiTerraformApplicationLimitations(TerraformStartApiIntegrationBase): @@ -176,7 +199,6 @@ def setUpClass(cls): "start-api", "--hook-name", "terraform", - "--beta-features", "-p", str(random_port()), ] @@ -222,6 +244,26 @@ def test_unsupported_limitations(self): "terraform_application": "terraform-api-simple-local-variables-limitation", "testing_urls": ["hello"], }, + { + "terraform_application": "terraform-v2-api-simple-multi-resource-link", + "testing_urls": ["hello"], + }, + { + "terraform_application": "terraform-v2-api-simple-local-resource-link", + "testing_urls": ["hello"], + }, + { + "terraform_application": "terraform-v2-openapi", + "testing_urls": ["hello"], + }, + { + "terraform_application": "terraform-v2-api-simple", + "testing_urls": ["hello"], + }, + { + "terraform_application": "terraform-v2-api-quick-create", + "testing_urls": ["hello"], + }, ] ) class TestStartApiTerraformApplicationLimitationsAfterApply(TerraformStartApiIntegrationApplyBase): @@ -240,22 +282,32 @@ def test_successful_request(self): not CI_OVERRIDE, "Skip Terraform test cases unless running in CI", ) +@parameterized_class( + [ + { + "terraform_application": "v1-lambda-authorizer", + "gateway_version": "v1", + }, + { + "terraform_application": "v2-lambda-authorizer", + "gateway_version": "v2", + }, + ] +) @pytest.mark.flaky(reruns=3) -class TestStartApiTerraformApplicationV1LambdaAuthorizers(TerraformStartApiIntegrationBase): - terraform_application = "v1-lambda-authorizer" - +class TestStartApiTerraformApplicationLambdaAuthorizers(TerraformStartApiIntegrationBase): def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) @parameterized.expand( [ - ("/hello", {"headers": {"myheader": "123"}}), - ("/hello-request", {"headers": {"myheader": "123"}, "params": {"mystring": "456"}}), - ("/hello-request-empty", {}), - ("/hello-request-empty", {"headers": {"foo": "bar"}}), + ("/hello", {"headers": {"myheader": "123"}}, ["v1", "v2"]), + ("/hello-request", {"headers": {"myheader": "123"}, "params": {"mystring": "456"}}, ["v1", "v2"]), ] ) - def test_invoke_authorizer(self, endpoint, parameters): + def test_invoke_authorizer(self, endpoint, parameters, applicable_gateway_versions): + if self.gateway_version not in applicable_gateway_versions: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") response = requests.get(self.url + endpoint, timeout=300, **parameters) self.assertEqual(response.status_code, 200) @@ -263,16 +315,20 @@ def test_invoke_authorizer(self, endpoint, parameters): @parameterized.expand( [ - ("/hello", {"headers": {"blank": "invalid"}}), - ("/hello-request", {"headers": {"blank": "invalid"}, "params": {"blank": "invalid"}}), + ("/hello", {"headers": {"blank": "invalid"}}, ["v1", "v2"]), + ("/hello-request", {"headers": {"blank": "invalid"}, "params": {"blank": "invalid"}}, ["v1", "v2"]), ] ) - def test_missing_authorizer_identity_source(self, endpoint, parameters): + def test_missing_authorizer_identity_source(self, endpoint, parameters, applicable_gateway_versions): + if self.gateway_version not in applicable_gateway_versions: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") response = requests.get(self.url + endpoint, timeout=300, **parameters) self.assertEqual(response.status_code, 401) def test_fails_token_header_validation_authorizer(self): + if self.gateway_version not in ["v1"]: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") response = requests.get(self.url + "/hello", timeout=300, headers={"myheader": "not valid"}) self.assertEqual(response.status_code, 401) @@ -282,20 +338,40 @@ def test_fails_token_header_validation_authorizer(self): not CI_OVERRIDE, "Skip Terraform test cases unless running in CI", ) +@parameterized_class( + [ + { + "terraform_application": "v1-lambda-authorizer", + "gateway_version": "v1", + }, + { + "terraform_application": "lambda-auth-openapi", + "gateway_version": "v1", + }, + { + "terraform_application": "terraform-v2-auth-openapi", + "gateway_version": "v2", + }, + { + "terraform_application": "v2-lambda-authorizer", + "gateway_version": "v2", + }, + ] +) @pytest.mark.flaky(reruns=3) -class TestStartApiTerraformApplicationOpenApiAuthorizer(TerraformStartApiIntegrationApplyBase): - terraform_application = "lambda-auth-openapi" - +class TestStartApiTerraformApplicationAuthorizerAfterApply(TerraformStartApiIntegrationApplyBase): def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) @parameterized.expand( [ - ("/hello", {"headers": {"myheader": "123"}}), - ("/hello-request", {"headers": {"myheader": "123"}, "params": {"mystring": "456"}}), + ("/hello", {"headers": {"myheader": "123"}}, ["v1", "v2"]), + ("/hello-request", {"headers": {"myheader": "123"}, "params": {"mystring": "456"}}, ["v1", "v2"]), ] ) - def test_successful_request(self, endpoint, params): + def test_successful_request(self, endpoint, params, applicable_gateway_versions): + if self.gateway_version not in applicable_gateway_versions: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") response = requests.get(self.url + endpoint, timeout=300, **params) self.assertEqual(response.status_code, 200) @@ -303,11 +379,20 @@ def test_successful_request(self, endpoint, params): @parameterized.expand( [ - ("/hello", {"headers": {"missin": "123"}}), - ("/hello-request", {"headers": {"notcorrect": "123"}, "params": {"abcde": "456"}}), + ("/hello", {"headers": {"missin": "123"}}, ["v1", "v2"]), + ("/hello-request", {"headers": {"notcorrect": "123"}, "params": {"abcde": "456"}}, ["v1", "v2"]), ] ) - def test_missing_identity_sources(self, endpoint, params): + def test_missing_identity_sources(self, endpoint, params, applicable_gateway_versions): + if self.gateway_version not in applicable_gateway_versions: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") response = requests.get(self.url + endpoint, timeout=300, **params) self.assertEqual(response.status_code, 401) + + def test_fails_token_header_validation_authorizer(self): + if self.gateway_version not in ["v1"]: + self.skipTest(f"This test case is not supported for {self.gateway_version} api gateway") + response = requests.get(self.url + "/hello", timeout=300, headers={"myheader": "not valid"}) + + self.assertEqual(response.status_code, 401) diff --git a/tests/integration/local/start_lambda/test_start_lambda_terraform_applications.py b/tests/integration/local/start_lambda/test_start_lambda_terraform_applications.py index 4c4a5f117e..3a24bfbd80 100644 --- a/tests/integration/local/start_lambda/test_start_lambda_terraform_applications.py +++ b/tests/integration/local/start_lambda/test_start_lambda_terraform_applications.py @@ -18,8 +18,6 @@ from docker.errors import APIError from parameterized import parameterized, parameterized_class -from samcli.commands._utils.experimental import EXPERIMENTAL_WARNING -from samcli.lib.utils.colors import Colored from tests.integration.local.common_utils import random_port from tests.integration.local.invoke.layer_utils import LayerUtils from tests.integration.local.start_lambda.start_lambda_api_integ_base import StartLambdaIntegBaseClass @@ -74,7 +72,6 @@ class TestLocalStartLambdaTerraformApplicationWithoutBuild(StartLambdaTerraformA terraform_application = "/testdata/invoke/terraform/simple_application_no_building_logic" template_path = None hook_name = "terraform" - beta_features = True def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) @@ -118,7 +115,6 @@ class TestLocalStartLambdaTerraformApplicationWithoutBuildCustomPlanFile(StartLa terraform_application = "/testdata/invoke/terraform/simple_application_no_building_logic" template_path = None hook_name = "terraform" - beta_features = True terraform_plan_file = "custom-plan.json" def setUp(self): @@ -175,7 +171,6 @@ class TestLocalStartLambdaTerraformApplicationWithLayersWithoutBuild(StartLambda pre_create_lambda_layers = ["simple_layer1", "simple_layer2", "simple_layer3", "simple_layer33", "simple_layer44"] template_path = None hook_name = "terraform" - beta_features = True @classmethod def setUpClass(cls): @@ -327,7 +322,6 @@ class TestInvalidTerraformApplicationThatReferToS3BucketNotCreatedYet(StartLambd terraform_application = "/testdata/invoke/terraform/invalid_no_local_code_project" template_path = None hook_name = "terraform" - beta_features = True @classmethod def setUpClass(cls): @@ -365,7 +359,6 @@ def test_invoke_function(self): command_list = self.get_start_lambda_command( port=self.port, hook_name=self.hook_name, - beta_features=self.beta_features, ) _, stderr, return_code = self._run_command(command_list, tf_application=self.working_dir) @@ -378,96 +371,6 @@ def test_invoke_function(self): self.assertNotEqual(return_code, 0) -@skipIf( - (not RUN_BY_CANARY and not CI_OVERRIDE), - "Skip Terraform test cases unless running in CI", -) -class TestLocalStartLambdaTerraformApplicationWithExperimentalPromptYes(StartLambdaTerraformApplicationIntegBase): - terraform_application = "/testdata/invoke/terraform/simple_application_no_building_logic" - template_path = None - hook_name = "terraform" - input = b"Y\n" - collect_start_lambda_process_output = True - - def setUp(self): - self.url = "http://127.0.0.1:{}".format(self.port) - self.lambda_client = boto3.client( - "lambda", - endpoint_url=self.url, - region_name="us-east-1", - use_ssl=False, - verify=False, - config=Config(signature_version=UNSIGNED, read_timeout=120, retries={"max_attempts": 0}), - ) - - @pytest.mark.flaky(reruns=3) - def test_invoke_function(self): - response = self.lambda_client.invoke(FunctionName="s3_lambda_function") - - response_body = json.loads(response.get("Payload").read().decode("utf-8")) - expected_response = json.loads('{"statusCode":200,"body":"{\\"message\\": \\"hello world\\"}"}') - - self.assertEqual(response_body, expected_response) - self.assertEqual(response.get("StatusCode"), 200) - - -@skipIf( - (not RUN_BY_CANARY and not CI_OVERRIDE), - "Skip Terraform test cases unless running in CI", -) -class TestLocalStartLambdaTerraformApplicationWithBetaFeatures(StartLambdaTerraformApplicationIntegBase): - terraform_application = "/testdata/invoke/terraform/simple_application_no_building_logic" - template_path = None - hook_name = "terraform" - beta_features = True - collect_start_lambda_process_output = True - - def setUp(self): - self.url = "http://127.0.0.1:{}".format(self.port) - self.lambda_client = boto3.client( - "lambda", - endpoint_url=self.url, - region_name="us-east-1", - use_ssl=False, - verify=False, - config=Config(signature_version=UNSIGNED, read_timeout=120, retries={"max_attempts": 0}), - ) - - @pytest.mark.flaky(reruns=3) - def test_invoke_function_and_warning_message_is_printed(self): - self.assertIn(Colored().yellow(EXPERIMENTAL_WARNING), self.start_lambda_process_error) - - -class TestLocalStartLambdaTerraformApplicationWithExperimentalPromptNo(StartLambdaTerraformApplicationIntegBase): - terraform_application = "/testdata/invoke/terraform/simple_application_no_building_logic" - template_path = None - hook_name = "terraform" - input = b"N\n" - collect_start_lambda_process_output = True - - def setUp(self): - self.url = "http://127.0.0.1:{}".format(self.port) - self.lambda_client = boto3.client( - "lambda", - endpoint_url=self.url, - region_name="us-east-1", - use_ssl=False, - verify=False, - config=Config(signature_version=UNSIGNED, read_timeout=120, retries={"max_attempts": 0}), - ) - - @skipIf( - not CI_OVERRIDE, - "Skip Terraform test cases unless running in CI", - ) - @pytest.mark.flaky(reruns=3) - def test_invoke_function(self): - self.assertRegex( - self.start_lambda_process_error, - "Terraform Support beta feature is not enabled.", - ) - - class TestLocalStartLambdaInvalidUsecasesTerraform(StartLambdaTerraformApplicationIntegBase): @classmethod def setUpClass(cls): @@ -497,60 +400,6 @@ def test_invalid_hook_name(self): ) self.assertNotEqual(return_code, 0) - def test_start_lambda_with_no_beta_feature(self): - command_list = self.get_start_lambda_command(hook_name="terraform", beta_features=False) - - _, stderr, return_code = self._run_command(command_list, tf_application=self.working_dir) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - - def test_start_lambda_with_no_beta_feature_option_in_samconfig_toml(self): - samconfig_toml_path = Path(self.working_dir).joinpath("samconfig.toml") - samconfig_lines = [ - bytes("version = 0.1" + os.linesep, "utf-8"), - bytes("[default.global.parameters]" + os.linesep, "utf-8"), - bytes("beta_features = false" + os.linesep, "utf-8"), - ] - with open(samconfig_toml_path, "wb") as file: - file.writelines(samconfig_lines) - - command_list = self.get_start_lambda_command(hook_name="terraform") - - _, stderr, return_code = self._run_command(command_list, tf_application=self.working_dir) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - # delete the samconfig file - try: - os.remove(samconfig_toml_path) - except (FileNotFoundError, PermissionError): - pass - - def test_start_lambda_with_no_beta_feature_option_in_environment_variables(self): - environment_variables = os.environ.copy() - environment_variables["SAM_CLI_BETA_TERRAFORM_SUPPORT"] = "False" - - command_list = self.get_start_lambda_command(hook_name="terraform") - _, stderr, return_code = self._run_command( - command_list, tf_application=self.working_dir, env=environment_variables - ) - - process_stderr = stderr.strip() - self.assertRegex( - process_stderr.decode("utf-8"), - "Terraform Support beta feature is not enabled.", - ) - self.assertEqual(return_code, 0) - def test_invalid_coexist_parameters(self): command_list = self.get_start_lambda_command(hook_name="terraform", template_path="path/template.yaml") _, stderr, return_code = self._run_command(command_list, tf_application=self.working_dir) @@ -572,7 +421,6 @@ class TestLocalStartLambdaTerraformApplicationWithLocalImageUri(StartLambdaTerra terraform_application = "/testdata/invoke/terraform/image_lambda_function_local_image_uri" template_path = None hook_name = "terraform" - beta_features = True functions = [ "module.image_lambda2.aws_lambda_function.this[0]", "image_lambda2", diff --git a/tests/integration/testdata/buildcmd/terraform/application_outside_root_directory/root_module/input_samconfig.yaml b/tests/integration/testdata/buildcmd/terraform/application_outside_root_directory/root_module/input_samconfig.yaml index 12d6d000fc..b594a3df22 100644 --- a/tests/integration/testdata/buildcmd/terraform/application_outside_root_directory/root_module/input_samconfig.yaml +++ b/tests/integration/testdata/buildcmd/terraform/application_outside_root_directory/root_module/input_samconfig.yaml @@ -2,7 +2,6 @@ version: 0.1 default: global: parameters: - beta_features: true hook_name: terraform build: parameters: diff --git a/tests/integration/testdata/start_api/terraform/lambda-auth-openapi/main.tf b/tests/integration/testdata/start_api/terraform/lambda-auth-openapi/main.tf index 8005fb4e95..33fa9b91a8 100644 --- a/tests/integration/testdata/start_api/terraform/lambda-auth-openapi/main.tf +++ b/tests/integration/testdata/start_api/terraform/lambda-auth-openapi/main.tf @@ -2,8 +2,14 @@ provider "aws" {} data "aws_region" "current" {} +resource "random_uuid" "unique_id" { + keepers = { + my_key = "my_key" + } +} + resource "aws_api_gateway_authorizer" "header_authorizer" { - name = "header-authorizer-open-api" + name = "header-authorizer-open-api_${random_uuid.unique_id.result}" rest_api_id = aws_api_gateway_rest_api.api.id authorizer_uri = aws_lambda_function.authorizer.invoke_arn authorizer_credentials = aws_iam_role.invocation_role.arn @@ -13,7 +19,7 @@ resource "aws_api_gateway_authorizer" "header_authorizer" { resource "aws_lambda_function" "authorizer" { filename = "lambda-functions.zip" - function_name = "authorizer-open-api" + function_name = "authorizer-open-api_${random_uuid.unique_id.result}" role = aws_iam_role.invocation_role.arn handler = "handlers.auth_handler" runtime = "python3.8" @@ -22,7 +28,7 @@ resource "aws_lambda_function" "authorizer" { resource "aws_lambda_function" "hello_endpoint" { filename = "lambda-functions.zip" - function_name = "hello-lambda-open-api" + function_name = "hello-lambda-open-api_${random_uuid.unique_id.result}" role = aws_iam_role.invocation_role.arn handler = "handlers.hello_handler" runtime = "python3.8" @@ -30,7 +36,7 @@ resource "aws_lambda_function" "hello_endpoint" { } resource "aws_api_gateway_rest_api" "api" { - name = "api-open-api" + name = "api-open-api_${random_uuid.unique_id.result}" body = jsonencode({ swagger = "2.0" info = { @@ -45,6 +51,7 @@ resource "aws_api_gateway_rest_api" "api" { x-amazon-apigateway-authtype = "custom" x-amazon-apigateway-authorizer = { type = "TOKEN" + identityValidationExpression = "^123$" authorizerUri = "arn:aws:apigateway:${data.aws_region.current.name}:lambda:path/2015-03-31/functions/${aws_lambda_function.authorizer.arn}/invocations" } } @@ -92,7 +99,7 @@ resource "aws_api_gateway_rest_api" "api" { } resource "aws_iam_role" "invocation_role" { - name = "iam-lambda-open-api" + name = "iam-lambda-open-api_${random_uuid.unique_id.result}" path = "/" assume_role_policy = < None: self.apigw_authorizer_name = "my_authorizer" self.apigw_integration_response_name = "my_integration_response" + self.apigwv2_api_name = "my_apigwv2_api" + self.apigwv2_api_quick_create_name = "my_apigwv2_api_quick_create" + self.apigwv2_route_name = "my_apigwv2_route" + self.apigwv2_stage_name = "my_apigwv2_stage" + self.apigwv2_integration_name = "my_apigwv2_integration" + self.apigwv2_authorizer_name = "my_authorizer_v2" + self.tf_function_common_properties: dict = { "function_name": self.zip_function_name, "architectures": ["x86_64"], @@ -347,6 +359,26 @@ def setUp(self) -> None: "provider_name": AWS_PROVIDER_NAME, } + self.tf_apigwv2_api_common_attributes: dict = { + "type": "aws_apigatewayv2_api", + "provider_name": AWS_PROVIDER_NAME, + } + + self.tf_apigwv2_route_common_attributes: dict = { + "type": "aws_apigatewayv2_route", + "provider_name": AWS_PROVIDER_NAME, + } + + self.tf_apigwv2_stage_common_attributes: dict = { + "type": "aws_apigatewayv2_stage", + "provider_name": AWS_PROVIDER_NAME, + } + + self.tf_apigwv2_integration_common_attributes: dict = { + "type": "aws_apigatewayv2_integration", + "provider_name": AWS_PROVIDER_NAME, + } + self.tf_lambda_function_resource_common_attributes: dict = { "type": "aws_lambda_function", "provider_name": AWS_PROVIDER_NAME, @@ -762,6 +794,194 @@ def setUp(self) -> None: "Metadata": {"SamResourceId": f"aws_api_gateway_rest_api.{self.apigw_rest_api_name}"}, } + self.tf_apigwv2_api_properties: dict = { + "name": "my_v2_api", + "protocol_type": "HTTP", + "cors_configuration": [ + { + "allow_credentials": True, + "allow_headers": ["Content-Type"], + "allow_methods": ["GET", "OPTIONS", "POST"], + "allow_origins": ["my-origin.com"], + "expose_headers": None, + "max_age": 500, + } + ], + } + + self.expected_cfn_apigwv2_api_properties: dict = { + "Name": "my_v2_api", + "ProtocolType": "HTTP", + "CorsConfiguration": { + "AllowCredentials": True, + "AllowHeaders": ["Content-Type"], + "AllowMethods": ["GET", "OPTIONS", "POST"], + "AllowOrigins": ["my-origin.com"], + "MaxAge": 500, + }, + } + + self.tf_apigwv2_api_resource: dict = { + **self.tf_apigwv2_api_common_attributes, + "values": self.tf_apigwv2_api_properties, + "address": f"aws_apigatewayv2_api.{self.apigwv2_api_name}", + "name": self.apigwv2_api_name, + } + + self.expected_cfn_apigwv2_api: dict = { + "Type": AWS_APIGATEWAY_V2_API, + "Properties": self.expected_cfn_apigwv2_api_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_api.{self.apigwv2_api_name}"}, + } + + self.tf_apigwv2_api_quick_create_properties: dict = { + "name": "my_v2_api_quick_create", + "protocol_type": "HTTP", + "target": "arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/" + "arn:aws:lambda:{region}:{account-id}:function:{function-name}/invocations", + "route_key": "my_route", + } + + self.expected_cfn_apigwv2_api_quick_create_properties: dict = { + "Name": "my_v2_api_quick_create", + "ProtocolType": "HTTP", + "Target": "arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/" + "arn:aws:lambda:{region}:{account-id}:function:{function-name}/invocations", + "RouteKey": "my_route", + } + + self.tf_apigwv2_api_quick_create_resource: dict = { + **self.tf_apigwv2_api_common_attributes, + "values": self.tf_apigwv2_api_quick_create_properties, + "address": f"aws_apigatewayv2_api.{self.apigwv2_api_quick_create_name}", + "name": self.apigwv2_api_quick_create_name, + } + + self.expected_cfn_apigwv2_api_quick_create: dict = { + "Type": AWS_APIGATEWAY_V2_API, + "Properties": self.expected_cfn_apigwv2_api_quick_create_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_api.{self.apigwv2_api_quick_create_name}"}, + } + + self.tf_apigwv2_route_properties: dict = { + "api_id": "aws_apigatewayv2_api.my_api.id", + "target": "aws_apigatewayv2_integration.example.id", + "route_key": "$default", + "operation_name": "my_operation", + } + + self.expected_cfn_apigwv2_route_properties: dict = { + "ApiId": "aws_apigatewayv2_api.my_api.id", + "Target": "aws_apigatewayv2_integration.example.id", + "RouteKey": "$default", + "OperationName": "my_operation", + } + + self.tf_apigwv2_route_resource: dict = { + **self.tf_apigwv2_route_common_attributes, + "values": self.tf_apigwv2_route_properties, + "address": f"aws_apigatewayv2_route.{self.apigwv2_route_name}", + "name": self.apigwv2_route_name, + } + + self.expected_cfn_apigwv2_route: dict = { + "Type": AWS_APIGATEWAY_V2_ROUTE, + "Properties": self.expected_cfn_apigwv2_route_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_route.{self.apigwv2_route_name}"}, + } + + self.tf_apigwv2_stage_properties: dict = { + "api_id": "aws_apigatewayv2_api.my_api.id", + "name": "example-stage", + "stage_variables": {"foo": "bar"}, + } + + self.expected_cfn_apigwv2_stage_properties: dict = { + "ApiId": "aws_apigatewayv2_api.my_api.id", + "StageName": "example-stage", + "StageVariables": {"foo": "bar"}, + } + + self.tf_apigwv2_stage_resource: dict = { + **self.tf_apigwv2_stage_common_attributes, + "values": self.tf_apigwv2_stage_properties, + "address": f"aws_apigatewayv2_stage.{self.apigwv2_stage_name}", + "name": self.apigwv2_stage_name, + } + + self.expected_cfn_apigwv2_stage: dict = { + "Type": AWS_APIGATEWAY_V2_STAGE, + "Properties": self.expected_cfn_apigwv2_stage_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_stage.{self.apigwv2_stage_name}"}, + } + + self.tf_apigwv2_integration_properties: dict = { + "api_id": "aws_apigatewayv2_api.my_api.id", + "integration_type": "AWS_PROXY", + "integration_method": "POST", + "integration_uri": "aws_lambda_function.HelloWorldFunction.invoke_arn", + "payload_format_version": "2.0", + } + + self.expected_cfn_apigwv2_integration_properties: dict = { + "ApiId": "aws_apigatewayv2_api.my_api.id", + "IntegrationType": "AWS_PROXY", + "IntegrationMethod": "POST", + "IntegrationUri": "aws_lambda_function.HelloWorldFunction.invoke_arn", + "PayloadFormatVersion": "2.0", + } + + self.tf_apigwv2_integration_resource: dict = { + **self.tf_apigwv2_integration_common_attributes, + "values": self.tf_apigwv2_integration_properties, + "address": f"aws_apigatewayv2_integration.{self.apigwv2_integration_name}", + "name": self.apigwv2_integration_name, + } + + self.expected_cfn_apigwv2_integration: dict = { + "Type": AWS_APIGATEWAY_V2_INTEGRATION, + "Properties": self.expected_cfn_apigwv2_integration_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_integration.{self.apigwv2_integration_name}"}, + } + + self.tf_apigwv2_authorizer_common_attributes: dict = { + "type": "aws_apigatewayv2_authorizer", + "provider_name": AWS_PROVIDER_NAME, + } + + self.tf_apigwv2_authorizer_properties: dict = { + "api_id": "aws_apigatewayv2_api.my_api.id", + "authorizer_type": "REQUEST", + "authorizer_uri": "aws_lambda_function.authorizerv2.invoke_arn", + "name": self.apigwv2_authorizer_name, + "authorizer_payload_format_version": "2.0", + "identity_sources": ["$request.header.hello"], + "enable_simple_responses": False, + } + + self.expected_cfn_apigwv2_authorizer_properties: dict = { + "ApiId": "aws_apigatewayv2_api.my_api.id", + "AuthorizerType": "REQUEST", + "AuthorizerUri": "aws_lambda_function.authorizerv2.invoke_arn", + "Name": self.apigwv2_authorizer_name, + "AuthorizerPayloadFormatVersion": "2.0", + "IdentitySource": ["$request.header.hello"], + "EnableSimpleResponses": False, + } + + self.tf_apigwv2_authorizer_resource: dict = { + **self.tf_apigwv2_authorizer_common_attributes, + "values": self.tf_apigwv2_authorizer_properties, + "address": f"aws_apigatewayv2_authorizer.{self.apigwv2_authorizer_name}", + "name": self.apigwv2_authorizer_name, + } + + self.expected_cfn_apigwv2_authorizer: dict = { + "Type": AWS_APIGATEWAY_V2_AUTHORIZER, + "Properties": self.expected_cfn_apigwv2_authorizer_properties, + "Metadata": {"SamResourceId": f"aws_apigatewayv2_authorizer.{self.apigwv2_authorizer_name}"}, + } + self.tf_json_with_root_module_only: dict = { "planned_values": { "root_module": { @@ -777,6 +997,12 @@ def setUp(self) -> None: self.tf_apigw_integration_resource, self.tf_apigw_authorizer_resource, self.tf_apigw_integration_response_resource, + self.tf_apigwv2_api_resource, + self.tf_apigwv2_api_quick_create_resource, + self.tf_apigwv2_route_resource, + self.tf_apigwv2_stage_resource, + self.tf_apigwv2_integration_resource, + self.tf_apigwv2_authorizer_resource, ] } } @@ -793,9 +1019,14 @@ def setUp(self) -> None: f"AwsApiGatewayMethodMyMethod{self.mock_logical_id_hash}": self.expected_cfn_apigw_method, f"AwsApiGatewayMethodMyMethodAuth{self.mock_logical_id_hash}": self.expected_cfn_apigw_method_with_auth, f"AwsApiGatewayAuthorizerMyAuthorizer{self.mock_logical_id_hash}": self.expected_cfn_apigw_authorizer, + f"AwsApigatewayv2ApiMyApigwv2Api{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_api, + f"AwsApigatewayv2ApiMyApigwv2ApiQuickCreate{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_api_quick_create, + f"AwsApigatewayv2RouteMyApigwv2Route{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_route, + f"AwsApigatewayv2StageMyApigwv2Stage{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_stage, + f"AwsApigatewayv2IntegrationMyApigwv2Integration{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_integration, + f"AwsApigatewayv2AuthorizerMyAuthorizerV2{self.mock_logical_id_hash}": self.expected_cfn_apigwv2_authorizer, }, } - self.tf_json_with_root_module_with_sam_metadata_resources: dict = { "planned_values": { "root_module": { diff --git a/tests/unit/hook_packages/terraform/hooks/prepare/test_property_builder.py b/tests/unit/hook_packages/terraform/hooks/prepare/test_property_builder.py index c5d81ff427..6e8cbfdb4a 100644 --- a/tests/unit/hook_packages/terraform/hooks/prepare/test_property_builder.py +++ b/tests/unit/hook_packages/terraform/hooks/prepare/test_property_builder.py @@ -10,6 +10,7 @@ _build_lambda_function_environment_property, _build_lambda_function_image_config_property, _check_image_config_value, + _get_cors_v2_api, ) from samcli.hook_packages.terraform.hooks.prepare.constants import REMOTE_DUMMY_VALUE @@ -214,3 +215,35 @@ def test_get_json_body_invalid(self, invalid_value): result = _get_json_body({"body": invalid_value}, Mock()) self.assertEqual(result, invalid_value) + + @parameterized.expand( + [ + ( + { + "cors_configuration": [ + { + "allow_credentials": True, + "allow_headers": ["Content-Type"], + "allow_methods": ["GET", "OPTIONS", "POST"], + "allow_origins": ["my-origin.com"], + "expose_headers": None, + "max_age": 500, + } + ], + }, + { + "AllowCredentials": True, + "AllowHeaders": ["Content-Type"], + "AllowMethods": ["GET", "OPTIONS", "POST"], + "AllowOrigins": ["my-origin.com"], + "MaxAge": 500, + }, + ), + ({"cors_configuration": None}, None), + ({"cors_configuration": []}, None), + ({"cors_configuration": [{"allow_credentials": True}]}, {"AllowCredentials": True}), + ] + ) + def test_get_cors_v2_api(self, tf_properties, expected): + response = _get_cors_v2_api(tf_properties, Mock()) + self.assertEqual(response, expected) diff --git a/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py b/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py index 11fd230223..dc80b6cbb6 100644 --- a/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py +++ b/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py @@ -12,11 +12,16 @@ TF_AWS_LAMBDA_FUNCTION, TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_RESOURCE, + TF_AWS_API_GATEWAY_V2_INTEGRATION, + TF_AWS_API_GATEWAY_V2_API, + TF_AWS_API_GATEWAY_V2_AUTHORIZER, ) from samcli.hook_packages.terraform.hooks.prepare.exceptions import ( GatewayAuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, GatewayAuthorizerToRestApiLocalVariablesLinkingLimitationException, GatewayMethodToGatewayAuthorizerLocalVariablesLinkingLimitationException, + GatewayV2AuthorizerToGatewayV2ApiLocalVariablesLinkingLimitationException, + GatewayV2AuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, InvalidResourceLinkingException, LocalVariablesLinkingLimitationException, ONE_LAMBDA_LAYER_LINKING_ISSUE_LINK, @@ -25,6 +30,8 @@ OneGatewayAuthorizerToLambdaFunctionLinkingLimitationException, OneGatewayAuthorizerToRestApiLinkingLimitationException, OneGatewayMethodToGatewayAuthorizerLinkingLimitationException, + OneGatewayV2AuthorizerToGatewayV2ApiLinkingLimitationException, + OneGatewayV2AuthorizerToLambdaFunctionLinkingLimitationException, OneLambdaLayerLinkingLimitationException, FunctionLayerLocalVariablesLinkingLimitationException, OneGatewayResourceToApiGatewayMethodLinkingLimitationException, @@ -47,6 +54,20 @@ GatewayResourceToApiGatewayIntegrationResponseLocalVariablesLinkingLimitationException, OneGatewayResourceToParentResourceLinkingLimitationException, GatewayResourceToParentResourceLocalVariablesLinkingLimitationException, + OneGatewayV2RouteToGatewayV2IntegrationLinkingLimitationException, + GatewayV2RouteToGatewayV2IntegrationLocalVariablesLinkingLimitationException, + OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException, + GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException, + OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException, + GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, + OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, + GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, + OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + OneGatewayV2StageToGatewayV2ApiLinkingLimitationException, + GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException, + OneGatewayV2RouteToGatewayV2AuthorizerLinkingLimitationException, + GatewayV2RouteToGatewayV2AuthorizerLocalVariablesLinkingLimitationException, ) from samcli.hook_packages.terraform.hooks.prepare.resource_linking import ( @@ -57,6 +78,8 @@ _link_gateway_authorizer_to_rest_api, _link_gateway_method_to_gateway_authorizer, _link_gateway_method_to_gateway_authorizer_call_back, + _link_gateway_v2_authorizer_to_api, + _link_gateway_v2_authorizer_to_lambda_function, _resolve_module_output, _resolve_module_variable, _build_module, @@ -93,6 +116,22 @@ ResourcePairExceptedDestination, _link_gateway_resource_to_parent_resource_call_back, _link_gateway_resources_to_parents, + _link_gateway_v2_route_to_integration, + API_GATEWAY_V2_INTEGRATION_RESOURCE_ADDRESS_PREFIX, + _link_gateway_v2_route_to_integration_callback, + _link_gateway_v2_integration_to_lambda_function_callback, + _link_gateway_v2_integration_to_lambda_function, + _extract_gateway_v2_integration_id_from_route_target_value, + _link_gateway_v2_integration_to_api, + API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + _link_gateway_v2_resource_to_api_callback, + _link_gateway_v2_route_to_api, + _link_gateway_v2_api_to_function, + _link_gateway_v2_api_to_function_callback, + _link_gateway_v2_stage_to_api, + _link_gateway_v2_route_to_authorizer_callback, + _link_gateway_v2_route_to_authorizer, + API_GATEWAY_V2_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, ) from samcli.hook_packages.terraform.hooks.prepare.utilities import get_configuration_address from samcli.hook_packages.terraform.hooks.prepare.types import ( @@ -2208,6 +2247,26 @@ def test_link_gateway_integration_to_function_call_back( _link_gateway_method_to_gateway_authorizer_call_back, "Could not link multiple Lambda Authorizers to one Gateway Method", ), + ( + _link_gateway_v2_route_to_integration_callback, + "Could not link multiple Gateway V2 Integrations to one Gateway V2 Route", + ), + ( + _link_gateway_v2_integration_to_lambda_function_callback, + "Could not link multiple lambda functions to one Gateway V2 Integration", + ), + ( + _link_gateway_v2_resource_to_api_callback, + "Could not link multiple Gateway V2 Apis to one Gateway V2 resource", + ), + ( + _link_gateway_v2_api_to_function_callback, + "Could not link a V2 API to more than one Lambda Function resources", + ), + ( + _link_gateway_v2_route_to_authorizer_callback, + "Could not link multiple Gateway V2 Authorizers to one Gateway V2 Route", + ), ] ) def test_linking_callbacks_raises_multiple_reference_exception(self, linking_call_back_method, expected_message): @@ -2222,6 +2281,11 @@ def test_linking_callbacks_raises_multiple_reference_exception(self, linking_cal (_link_gateway_resource_to_gateway_resource_call_back,), (_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back,), (_link_gateway_method_to_gateway_authorizer_call_back,), + (_link_gateway_v2_route_to_integration_callback,), + (_link_gateway_v2_integration_to_lambda_function_callback,), + (_link_gateway_v2_resource_to_api_callback,), + (_link_gateway_v2_api_to_function_callback,), + (_link_gateway_v2_route_to_authorizer_callback,), ] ) def test_linking_callbacks_skips_empty_references(self, linking_call_back_method): @@ -2499,3 +2563,570 @@ def test_link_gateway_method_to_gateway_authorizer( ) mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @patch( + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._extract_gateway_v2_integration_id_from_route_target_value" + ) + @patch( + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_route_to_integration_callback" + ) + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_route_to_gateway_v2_integration( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_route_to_integration_callback, + mock_integration_id_extractor_function, + ): + routes_v2_cfn_resources = Mock() + routes_v2_config_resources = Mock() + integrations_v2_tf_resources = Mock() + + _link_gateway_v2_route_to_integration( + routes_v2_config_resources, routes_v2_cfn_resources, integrations_v2_tf_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2IntegrationLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2IntegrationLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=routes_v2_cfn_resources, + source_resource_tf_config=routes_v2_config_resources, + destination_resource_tf=integrations_v2_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_INTEGRATION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="target", + cfn_link_field_name="Target", + cfn_resource_update_call_back_function=mock_link_gateway_v2_route_to_integration_callback, + linking_exceptions=mock_resource_linking_exceptions(), + tf_destination_value_extractor_from_link_field_value_function=mock_integration_id_extractor_function, + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"Target": "invoke_arn"}, + }, + [LogicalIdReference(value="FunctionA", resource_type=TF_AWS_LAMBDA_FUNCTION)], + {"Fn::Join": ["/", ["integrations", {"Ref": "FunctionA"}]]}, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"Target": "invoke_arn"}, + }, + [ExistingResourceReference("invoke_arn")], + "integrations/invoke_arn", + ), + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"Target": "invoke_arn"}, + }, + [ExistingResourceReference("integrations/invoke_arn")], + "integrations/invoke_arn", + ), + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"Target": "invoke_arn"}, + }, + [LogicalIdReference(value="Integration", resource_type=TF_AWS_API_GATEWAY_V2_INTEGRATION)], + {"Fn::Join": ["/", ["integrations", {"Ref": "Integration"}]]}, + ), + ] + ) + def test__link_gateway_v2_route_to_integration_callback(self, input_gateway_v2_route, logical_ids, expected_route): + gateway_resource = deepcopy(input_gateway_v2_route) + _link_gateway_v2_route_to_integration_callback(gateway_resource, logical_ids) + input_gateway_v2_route["Properties"]["Target"] = expected_route + self.assertEqual(gateway_resource, input_gateway_v2_route) + + @patch( + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_integration_to_lambda_function_callback" + ) + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_integration_to_lambda_function( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_integration_to_lambda_function_callback, + ): + integrations_v2_cfn_resources = Mock() + integrations_v2_config_resources = Mock() + lambda_function_tf_resources = Mock() + + _link_gateway_v2_integration_to_lambda_function( + integrations_v2_config_resources, integrations_v2_cfn_resources, lambda_function_tf_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=integrations_v2_cfn_resources, + source_resource_tf_config=integrations_v2_config_resources, + destination_resource_tf=lambda_function_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="integration_uri", + cfn_link_field_name="IntegrationUri", + cfn_resource_update_call_back_function=mock_link_gateway_v2_integration_to_lambda_function_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": {"IntegrationUri": "invoke_arn"}, + }, + [LogicalIdReference(value="FunctionA", resource_type=TF_AWS_LAMBDA_FUNCTION)], + { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": ["FunctionA", "Arn"]}, + "/invocations", + ], + ] + }, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": {"IntegrationUri": "invoke_arn"}, + }, + [ExistingResourceReference("invoke_arn")], + "invoke_arn", + ), + ] + ) + def test_link_gateway_v2_integration_to_lambda_function_callback( + self, input_gateway_v2_integration, logical_ids, expected_route + ): + gateway_resource = deepcopy(input_gateway_v2_integration) + _link_gateway_v2_integration_to_lambda_function_callback(gateway_resource, logical_ids) + input_gateway_v2_integration["Properties"]["IntegrationUri"] = expected_route + self.assertEqual(gateway_resource, input_gateway_v2_integration) + + @parameterized.expand( + [ + ("integrations/invokeArn", "invokeArn"), + ("invokeArn", "invokeArn"), + ] + ) + def test_extract_gateway_v2_integration_id_from_route_target_value(self, input_value, expected_output): + output = _extract_gateway_v2_integration_id_from_route_target_value(input_value) + self.assertEqual(output, expected_output) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_resource_to_api_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_integration_to_gateway_v2_api( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_integration_to_api_callback, + ): + integrations_v2_cfn_resources = Mock() + integrations_v2_config_resources = Mock() + apis_v2_tf_resources = Mock() + + _link_gateway_v2_integration_to_api( + integrations_v2_config_resources, integrations_v2_cfn_resources, apis_v2_tf_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=integrations_v2_cfn_resources, + source_resource_tf_config=integrations_v2_config_resources, + destination_resource_tf=apis_v2_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=mock_link_gateway_v2_integration_to_api_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_resource_to_api_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_route_to_gateway_v2_api( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_route_to_api_callback, + ): + routes_v2_cfn_resources = Mock() + routes_v2_config_resources = Mock() + apis_v2_tf_resources = Mock() + + _link_gateway_v2_route_to_api(routes_v2_config_resources, routes_v2_cfn_resources, apis_v2_tf_resources) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=routes_v2_cfn_resources, + source_resource_tf_config=routes_v2_config_resources, + destination_resource_tf=apis_v2_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=mock_link_gateway_v2_route_to_api_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_resource_to_api_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_stage_to_gateway_v2_api( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_stage_to_api_callback, + ): + stages_v2_cfn_resources = Mock() + stages_v2_config_resources = Mock() + apis_v2_tf_resources = Mock() + + _link_gateway_v2_stage_to_api(stages_v2_config_resources, stages_v2_cfn_resources, apis_v2_tf_resources) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2StageToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=stages_v2_cfn_resources, + source_resource_tf_config=stages_v2_config_resources, + destination_resource_tf=apis_v2_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=mock_link_gateway_v2_stage_to_api_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": {"ApiId": "api_id"}, + }, + [LogicalIdReference(value="myapi", resource_type=TF_AWS_API_GATEWAY_V2_API)], + {"Ref": "myapi"}, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": {"ApiId": "api_id"}, + }, + [ExistingResourceReference("myapi_arn")], + "myapi_arn", + ), + ] + ) + def test_link_gateway_v2_integration_to_api_callback( + self, input_gateway_v2_integration, logical_ids, expected_api_reference + ): + gateway_resource = deepcopy(input_gateway_v2_integration) + _link_gateway_v2_resource_to_api_callback(gateway_resource, logical_ids) + input_gateway_v2_integration["Properties"]["ApiId"] = expected_api_reference + self.assertEqual(gateway_resource, input_gateway_v2_integration) + + @patch( + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_authorizer_to_lambda_function_call_back" + ) + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_authorizer_to_lambda_function( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_authorizer_to_lambda_function_call_back, + ): + v2_authorizer_cfn_resources = Mock() + v2_authorizer_config_resources = Mock() + lambda_function_resources = Mock() + + _link_gateway_v2_authorizer_to_lambda_function( + v2_authorizer_config_resources, v2_authorizer_cfn_resources, lambda_function_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2AuthorizerToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2AuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=v2_authorizer_cfn_resources, + source_resource_tf_config=v2_authorizer_config_resources, + destination_resource_tf=lambda_function_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="authorizer_uri", + cfn_link_field_name="AuthorizerUri", + cfn_resource_update_call_back_function=mock_link_gateway_authorizer_to_lambda_function_call_back, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_resource_to_api_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_authorizer_to_api( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_resource_to_api_callback, + ): + v2_authorizer_cfn_resources = Mock() + v2_authorizer_config_resources = Mock() + v2_api_resources = Mock() + + _link_gateway_v2_authorizer_to_api( + v2_authorizer_config_resources, v2_authorizer_cfn_resources, v2_api_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2AuthorizerToGatewayV2ApiLinkingLimitationException, + local_variable_linking_exception=GatewayV2AuthorizerToGatewayV2ApiLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=v2_authorizer_cfn_resources, + source_resource_tf_config=v2_authorizer_config_resources, + destination_resource_tf=v2_api_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="api_id", + cfn_link_field_name="ApiId", + cfn_resource_update_call_back_function=mock_link_gateway_v2_resource_to_api_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_api_to_function_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_api_to_lambda_function( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_api_to_function_callback, + ): + api_v2_cfn_resources = Mock() + quick_create_resource = TFResource("resource_address", "type", Mock(), {"target": ConstantValue("val")}) + combined_resources = { + "ResourceA": quick_create_resource, + "ResourceB": TFResource("resource_address", "type", Mock(), {"name": ConstantValue("MyAPI")}), + } + expected_quick_create_resource = {"ResourceA": quick_create_resource} + lambda_function_tf_resources = Mock() + + _link_gateway_v2_api_to_function(combined_resources, api_v2_cfn_resources, lambda_function_tf_resources) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=api_v2_cfn_resources, + source_resource_tf_config=expected_quick_create_resource, + destination_resource_tf=lambda_function_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], + terraform_link_field_name="target", + cfn_link_field_name="Target", + cfn_resource_update_call_back_function=mock_link_gateway_v2_api_to_function_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": {"Target": "functionA.invoke_arn"}, + }, + [LogicalIdReference(value="FunctionA", resource_type=TF_AWS_LAMBDA_FUNCTION)], + { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FunctionA.Arn}/invocations" + }, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": {"Target": "functionA.invoke_arn"}, + }, + [ExistingResourceReference("myapi_arn")], + "myapi_arn", + ), + ] + ) + def test_link_gateway_v2_api_to_function_callback(self, input_gateway_v2_api, logical_ids, expected_api_reference): + gateway_resource = deepcopy(input_gateway_v2_api) + _link_gateway_v2_api_to_function_callback(gateway_resource, logical_ids) + input_gateway_v2_api["Properties"]["Target"] = expected_api_reference + self.assertEqual(gateway_resource, input_gateway_v2_api) + + @patch( + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_route_to_authorizer_callback" + ) + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_route_to_authorizer( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_route_to_authorizer_callback, + ): + routes_v2_cfn_resources = Mock() + routes_v2_config_resources = Mock() + authorizers_v2_tf_resources = Mock() + + _link_gateway_v2_route_to_authorizer( + routes_v2_config_resources, routes_v2_cfn_resources, authorizers_v2_tf_resources + ) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2RouteToGatewayV2AuthorizerLinkingLimitationException, + local_variable_linking_exception=GatewayV2RouteToGatewayV2AuthorizerLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=routes_v2_cfn_resources, + source_resource_tf_config=routes_v2_config_resources, + destination_resource_tf=authorizers_v2_tf_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_V2_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="authorizer_id", + cfn_link_field_name="AuthorizerId", + cfn_resource_update_call_back_function=mock_link_gateway_v2_route_to_authorizer_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"AuthorizerId": "auth_id"}, + }, + [LogicalIdReference(value="my_auth_resource", resource_type=TF_AWS_API_GATEWAY_V2_AUTHORIZER)], + {"Ref": "my_auth_resource"}, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": {"AuthorizerId": "auth_id"}, + }, + [ExistingResourceReference("my_auth_id")], + "my_auth_id", + ), + ] + ) + def test_link_gateway_v2_route_to_authorizer_callback( + self, input_gateway_v2_route, logical_ids, expected_authorizer_reference + ): + gateway_resource = deepcopy(input_gateway_v2_route) + _link_gateway_v2_route_to_authorizer_callback(gateway_resource, logical_ids) + input_gateway_v2_route["Properties"]["AuthorizerId"] = expected_authorizer_reference + self.assertEqual(gateway_resource, input_gateway_v2_route) diff --git a/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py b/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py index 60d377aa0c..ea5239f72d 100644 --- a/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py +++ b/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py @@ -6,6 +6,7 @@ from tests.unit.hook_packages.terraform.hooks.prepare.prepare_base import PrepareHookUnitBase from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( + AWS_API_GATEWAY_V2_AUTHORIZER_PROPERTY_BUILDER_MAPPING, AWS_LAMBDA_FUNCTION_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_RESOURCE_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_REST_API_PROPERTY_BUILDER_MAPPING, @@ -14,6 +15,10 @@ AWS_API_GATEWAY_INTEGRATION_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_AUTHORIZER_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_INTEGRATION_RESPONSE_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_V2_API_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_V2_ROUTE_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_V2_STAGE_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_V2_INTEGRATION_PROPERTY_BUILDER_MAPPING, ) from samcli.hook_packages.terraform.hooks.prepare.constants import ( REMOTE_DUMMY_VALUE, @@ -1157,6 +1162,42 @@ def test_translating_apigw_integration_response_method(self): ) self.assertEqual(translated_cfn_properties, self.expected_internal_apigw_integration_response_properties) + def test_translating_apigwv2_api(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_api_properties, AWS_API_GATEWAY_V2_API_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_api_properties) + + def test_translating_apigwv2_api_quick_create(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_api_quick_create_properties, AWS_API_GATEWAY_V2_API_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_api_quick_create_properties) + + def test_translating_apigwv2_route(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_route_properties, AWS_API_GATEWAY_V2_ROUTE_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_route_properties) + + def test_translating_apigwv2_stage(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_stage_properties, AWS_API_GATEWAY_V2_STAGE_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_stage_properties) + + def test_translating_apigwv2_integration(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_integration_properties, AWS_API_GATEWAY_V2_INTEGRATION_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_integration_properties) + + def test_translating_apigwv2_authorizer(self): + translated_cfn_properties = _translate_properties( + self.tf_apigwv2_authorizer_properties, AWS_API_GATEWAY_V2_AUTHORIZER_PROPERTY_BUILDER_MAPPING, Mock() + ) + self.assertEqual(translated_cfn_properties, self.expected_cfn_apigwv2_authorizer_properties) + class TestUnresolvableAttributeCheck(TestCase): @patch("samcli.hook_packages.terraform.hooks.prepare.translate.RESOURCE_TRANSLATOR_MAPPING")