diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 274da04dd34d2..0dff7f7d8b1de 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -425,6 +425,7 @@ def __init__(self, scope: Scope, value: Any): FnTransform: Final[str] = "Fn::Transform" FnSelect: Final[str] = "Fn::Select" FnSplit: Final[str] = "Fn::Split" +FnGetAZs: Final[str] = "Fn::GetAZs" INTRINSIC_FUNCTIONS: Final[set[str]] = { RefKey, FnIfKey, @@ -437,6 +438,7 @@ def __init__(self, scope: Scope, value: Any): FnTransform, FnSelect, FnSplit, + FnGetAZs, } diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index 8ba7e301a9125..60f2fd89dcadf 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -3,6 +3,10 @@ import re from typing import Any, Final, Generic, Optional, TypeVar +from botocore.exceptions import ClientError + +from localstack.aws.api.ec2 import AvailabilityZoneList, DescribeAvailabilityZonesResult +from localstack.aws.connect import connect_to from localstack.services.cloudformation.engine.transformers import ( Transformer, execute_macro, @@ -677,7 +681,7 @@ def _compute_fn_select(args: list[Any]) -> Any: def visit_node_intrinsic_function_fn_split( self, node_intrinsic_function: NodeIntrinsicFunction - ) -> PreprocEntityDelta: + ): # TODO: add further support for schema validation arguments_delta = self.visit(node_intrinsic_function.arguments) arguments_before = arguments_delta.before @@ -703,6 +707,47 @@ def _compute_fn_split(args: list[Any]) -> Any: return PreprocEntityDelta(before=before, after=after) + def visit_node_intrinsic_function_fn_get_a_zs( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + # TODO: add further support for schema validation + arguments_delta = self.visit(node_intrinsic_function.arguments) + arguments_before = arguments_delta.before + arguments_after = arguments_delta.after + + def _compute_fn_get_a_zs(region) -> Any: + if not isinstance(region, str): + raise RuntimeError(f"Invalid region value for Fn::GetAZs: '{region}'") + + if not region: + region = self._change_set.region_name + + account_id = self._change_set.account_id + ec2_client = connect_to(aws_access_key_id=account_id, region_name=region).ec2 + try: + describe_availability_zones_result: DescribeAvailabilityZonesResult = ( + ec2_client.describe_availability_zones() + ) + except ClientError: + raise RuntimeError( + "Could not describe zones availability whilst evaluating Fn::GetAZs" + ) + availability_zones: AvailabilityZoneList = describe_availability_zones_result[ + "AvailabilityZones" + ] + azs = [az["ZoneName"] for az in availability_zones] + return azs + + before = Nothing + if not is_nothing(arguments_before): + before = _compute_fn_get_a_zs(arguments_before) + + after = Nothing + if not is_nothing(arguments_after): + after = _compute_fn_get_a_zs(arguments_after) + + return PreprocEntityDelta(before=before, after=after) + def visit_node_intrinsic_function_fn_find_in_map( self, node_intrinsic_function: NodeIntrinsicFunction ) -> PreprocEntityDelta: diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py index 88584d3ad800b..6f42186bf20ee 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py @@ -128,6 +128,11 @@ def visit_node_intrinsic_function_fn_split( ): self.visit_children(node_intrinsic_function) + def visit_node_intrinsic_function_fn_get_a_zs( + self, node_intrinsic_function: NodeIntrinsicFunction + ): + self.visit_children(node_intrinsic_function) + def visit_node_intrinsic_function_fn_sub(self, node_intrinsic_function: NodeIntrinsicFunction): self.visit_children(node_intrinsic_function) diff --git a/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_ec2.py b/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_ec2.py index 9907349aacfa0..df1d786717ae0 100644 --- a/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_ec2.py +++ b/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_ec2.py @@ -70,7 +70,7 @@ def test_simple_route_table_creation(deploy_cfn_template, aws_client, snapshot): # ec2.describe_route_tables(RouteTableIds=[route_table_id]) -@pytest.mark.skip(reason="CFNV2:Fn::Select, CFNV2:Fn::GatAZs") +@pytest.mark.skip(reason="CFNV2:Other") @markers.aws.validated def test_vpc_creates_default_sg(deploy_cfn_template, aws_client): result = deploy_cfn_template( @@ -109,7 +109,6 @@ def test_cfn_with_multiple_route_tables(deploy_cfn_template, aws_client): assert len(resp["RouteTables"]) == 4 -@pytest.mark.skip(reason="CFNV2:Fn::Select, CFNV2:Fn::GatAZs") @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=["$..PropagatingVgws", "$..Tags", "$..Tags..Key", "$..Tags..Value"] @@ -165,7 +164,6 @@ def test_dhcp_options(aws_client, deploy_cfn_template, snapshot): snapshot.match("description", response["DhcpOptions"][0]) -@pytest.mark.skip(reason="CFNV2:Fn::Select, CFNV2:Fn::GatAZs") @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ diff --git a/tests/aws/services/cloudformation/v2/ported_from_v1/test_template_engine.py b/tests/aws/services/cloudformation/v2/ported_from_v1/test_template_engine.py index 6f507d97fbf8f..d163b595e3365 100644 --- a/tests/aws/services/cloudformation/v2/ported_from_v1/test_template_engine.py +++ b/tests/aws/services/cloudformation/v2/ported_from_v1/test_template_engine.py @@ -197,7 +197,6 @@ def test_cidr_function(self, deploy_cfn_template): assert deployed.outputs["Address"] == "10.0.0.0/24" - @pytest.mark.skip(reason="CFNV2:Fn::GetAZs") @pytest.mark.parametrize( "region", [