diff --git a/src/cfnlint/rules/resources/ectwo/VpcSubnetCidr.py b/src/cfnlint/rules/resources/ectwo/VpcSubnetCidr.py new file mode 100644 index 0000000000..9266908ff5 --- /dev/null +++ b/src/cfnlint/rules/resources/ectwo/VpcSubnetCidr.py @@ -0,0 +1,197 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +import logging +from collections import deque +from ipaddress import IPv4Network, IPv6Network, ip_network +from typing import Any, Iterator + +from cfnlint.context import Path +from cfnlint.jsonschema import ValidationError, ValidationResult, Validator +from cfnlint.rules.helpers import get_value_from_path +from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword + +LOGGER = logging.getLogger(__name__) + + +class VpcSubnetCidr(CfnLintKeyword): + id = "E3059" + shortdesc = "Validate subnet CIDRs are within the CIDRs of the VPC" + description = ( + "When specifying subnet CIDRs for a VPC the subnet CIDRs " + "most be within the VPC CIDRs" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html" + tags = ["resources", "ec2", "vpc", "subnet"] + + def __init__(self) -> None: + super().__init__( + keywords=[ + "Resources/AWS::EC2::VPC/Properties", + ], + ) + + def _validate_subnets( + self, + source: IPv4Network | IPv6Network, + destination: IPv4Network | IPv6Network, + ) -> bool: + if isinstance(source, IPv4Network) and isinstance(destination, IPv4Network): + if source.subnet_of(destination): + return True + return False + elif isinstance(source, IPv6Network) and isinstance(destination, IPv6Network): + if source.subnet_of(destination): + return True + return False + return False + + def _create_network(self, cidr: Any) -> IPv4Network | IPv6Network | None: + if not isinstance(cidr, str): + return None + + try: + return ip_network(cidr) + except Exception as e: + LOGGER.debug(f"Unable to create network from {cidr}", e) + + return None + + def _get_vpc_cidrs( + self, validator: Validator, instance: dict[str, Any] + ) -> Iterator[tuple[IPv4Network | IPv6Network | None, Validator]]: + for key in [ + "Ipv4IpamPoolId", + "Ipv6IpamPoolId", + "Ipv6Pool", + "AmazonProvidedIpv6CidrBlock", + ]: + for value, value_validator in get_value_from_path( + validator, + instance, + deque([key]), + ): + if value is None: + continue + yield None, value_validator + + for key in ["CidrBlock", "Ipv6CidrBlock"]: + for cidr, cidr_validator in get_value_from_path( + validator, + instance, + deque([key]), + ): + + if cidr is None: + continue + yield self._create_network(cidr), cidr_validator + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any] + ) -> ValidationResult: + + if not validator.cfn.graph: + return + + vpc_ipv4_networks: list[IPv4Network] = [] + vpc_ipv6_networks: list[IPv6Network] = [] + for vpc_network, _ in self._get_vpc_cidrs(validator, instance): + if not vpc_network: + return + if isinstance(vpc_network, IPv4Network): + vpc_ipv4_networks.append(vpc_network) + # you can't specify IPV6 networks on a VPC + + template_validator = validator.evolve( + context=validator.context.evolve(path=Path()) + ) + + # dynamic vpc network (using IPAM or AWS provided) + # allows to validate subnet overlapping even if using + # dynamic networks + has_dynamic_network = False + + for source, _ in validator.cfn.graph.graph.in_edges( + validator.context.path.path[1] + ): + if ( + validator.cfn.graph.graph.nodes[source].get("resource_type") + == "AWS::EC2::VPCCidrBlock" + ): + for cidr_props, cidr_validator in get_value_from_path( + template_validator, + validator.cfn.template, + deque(["Resources", source, "Properties"]), + ): + for cidr_network, _ in self._get_vpc_cidrs( + cidr_validator, cidr_props + ): + if not cidr_network: + has_dynamic_network = True + continue + if isinstance(cidr_network, IPv4Network): + vpc_ipv4_networks.append(cidr_network) + else: + vpc_ipv6_networks.append(cidr_network) + + subnets: list[tuple[IPv4Network | IPv6Network, deque]] = [] + for source, _ in validator.cfn.graph.graph.in_edges( + validator.context.path.path[1] + ): + if ( + validator.cfn.graph.graph.nodes[source].get("resource_type") + == "AWS::EC2::Subnet" + ): + for subnet_props, source_validator in get_value_from_path( + template_validator, + validator.cfn.template, + deque(["Resources", source, "Properties"]), + ): + for subnet_network, subnet_validator in self._get_vpc_cidrs( + source_validator, subnet_props + ): + if not subnet_network: + continue + + subnets.append( + (subnet_network, subnet_validator.context.path.path) + ) + if has_dynamic_network: + continue + if not any( + self._validate_subnets( + subnet_network, + vpc_network, + ) + for vpc_network in vpc_ipv4_networks + vpc_ipv6_networks + ): + if isinstance(subnet_network, IPv4Network): + # Every VPC has to have a ipv4 network + # we continue if there isn't one + if not vpc_ipv4_networks: + continue + reprs = ( + "is not a valid subnet of " + f"{[f'{str(v)}' for v in vpc_ipv4_networks]!r}" + ) + else: + if not vpc_ipv6_networks: + reprs = ( + "is specified on a VPC that has " + "no ipv6 networks defined" + ) + else: + reprs = ( + "is not a valid subnet of " + f"{[f'{str(v)}' for v in vpc_ipv6_networks]!r}" + ) + yield ValidationError( + (f"{str(subnet_network)!r} {reprs}"), + rule=self, + path_override=subnet_validator.context.path.path, + ) + continue diff --git a/src/cfnlint/rules/resources/ectwo/VpcSubnetOverlap.py b/src/cfnlint/rules/resources/ectwo/VpcSubnetOverlap.py new file mode 100644 index 0000000000..2c61832d76 --- /dev/null +++ b/src/cfnlint/rules/resources/ectwo/VpcSubnetOverlap.py @@ -0,0 +1,136 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +import logging +from collections import deque +from ipaddress import IPv4Network, IPv6Network, ip_network +from typing import Any + +from cfnlint.context.conditions import Unsatisfiable +from cfnlint.helpers import ensure_list, is_function +from cfnlint.jsonschema import ValidationError, ValidationResult, Validator +from cfnlint.rules.helpers import get_value_from_path +from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword + +LOGGER = logging.getLogger(__name__) + + +class VpcSubnetOverlap(CfnLintKeyword): + id = "E3060" + shortdesc = "Validate subnet CIDRs do not overlap with other subnets" + description = ( + "When specifying subnet CIDRs for a VPC the subnet CIDRs " + "most not overlap with eachother" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html" + tags = ["resources", "ec2", "vpc", "subnet"] + + def __init__(self) -> None: + super().__init__( + keywords=[ + "Resources/AWS::EC2::Subnet/Properties", + ], + ) + self._subnets: dict[str, list[tuple[IPv4Network | IPv6Network, dict]]] = {} + + def initialize(self, cfn): + self._subnets = {} + return super().initialize(cfn) + + def _validate_subnets( + self, + source: IPv4Network | IPv6Network, + destination: IPv4Network | IPv6Network, + ) -> bool: + if isinstance(source, IPv4Network) and isinstance(destination, IPv4Network): + if source.overlaps(destination): + return True + return False + elif isinstance(source, IPv6Network) and isinstance(destination, IPv6Network): + if source.overlaps(destination): + return True + return False + return False + + def _create_network(self, cidr: Any) -> IPv4Network | IPv6Network | None: + if not isinstance(cidr, str): + return None + + try: + return ip_network(cidr) + except Exception as e: + LOGGER.debug(f"Unable to create network from {cidr}", e) + + return None + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any] + ) -> ValidationResult: + + for vpc_id, vpc_validator in get_value_from_path( + validator=validator, instance=instance, path=deque(["VpcId"]) + ): + + if not isinstance(vpc_id, (str, dict)): + return + + fn_k, fn_v = is_function(vpc_id) + if fn_k == "Fn::GetAtt": + vpc_id = ensure_list(fn_v)[0].split(".")[0] + elif fn_k == "Ref": + vpc_id = fn_v + elif fn_k: + # its a function that we can't resolve + return + + if not validator.is_type(vpc_id, "string"): + return + if vpc_id not in self._subnets: + self._subnets[vpc_id] = [] + + for key in ["CidrBlock", "Ipv6CidrBlock"]: + for cidr_block, cidr_block_validator in get_value_from_path( + validator=vpc_validator, instance=instance, path=deque([key]) + ): + + cidr_network = self._create_network(cidr_block) + if not cidr_network: + continue + + for saved_subnet, conditions in self._subnets[vpc_id]: + # attempt to validate if the saved conditions comply + # with these conditions + try: + cidr_block_validator.evolve( + context=cidr_block_validator.context.evolve( + conditions=cidr_block_validator.context.conditions.evolve( + conditions + ) + ) + ) + except Unsatisfiable: + continue + + # now we can evaluate if they overlap + if self._validate_subnets( + cidr_network, + saved_subnet, + ): + yield ValidationError( + ( + f"{str(cidr_network)!r} overlaps " + f"with {str(saved_subnet)!r}" + ), + rule=self, + path=deque( + list(cidr_block_validator.context.path.path)[1:] + ), + ) + + self._subnets[vpc_id].append( + (cidr_network, cidr_block_validator.context.conditions.status) + ) diff --git a/test/fixtures/templates/integration/aws-ec2-subnet.yaml b/test/fixtures/templates/integration/aws-ec2-subnet.yaml index f2ad679c57..0807bbf5a0 100644 --- a/test/fixtures/templates/integration/aws-ec2-subnet.yaml +++ b/test/fixtures/templates/integration/aws-ec2-subnet.yaml @@ -27,6 +27,6 @@ Resources: Type: AWS::EC2::Subnet Properties: VpcId: !Ref Vpc - CidrBlock: 10.0.1.0/24 + CidrBlock: 10.0.2.0/24 Ipv4IpamPoolId: test Ipv4NetmaskLength: 10 diff --git a/test/unit/rules/resources/ec2/test_vpc_subnet_cidr.py b/test/unit/rules/resources/ec2/test_vpc_subnet_cidr.py new file mode 100644 index 0000000000..9bdb0452a4 --- /dev/null +++ b/test/unit/rules/resources/ec2/test_vpc_subnet_cidr.py @@ -0,0 +1,348 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.resources.ectwo.VpcSubnetCidr import VpcSubnetCidr + + +@pytest.fixture(scope="module") +def rule(): + rule = VpcSubnetCidr() + yield rule + + +_template = { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/24", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "SecurityGroupIngress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "VpcId": {"Fn::GetAtt": ["Vpc", "DefaultSecurityGroup"]}, + }, + }, + } +} + + +@pytest.mark.parametrize( + "name,instance,template,path,expected", + [ + ( + "Valid with no Private Ip Address", + { + "CidrBlock": "10.0.0.0/16", + }, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Valid with bad ip address on VPC", + { + "CidrBlock": "10.0.0/16", + }, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Valid with a bad subnet IP Address", + { + "CidrBlock": "10.0.0.0/16", + }, + { + "Parameters": {"VpcCidr": {"Type": "String"}}, + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0/24", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Parameter CIDR", + { + "CidrBlock": {"Ref": "VpcCidr"}, + }, + { + "Parameters": {"VpcCidr": {"Type": "String"}}, + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/24", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Empty CidrBlocks should continue", + {}, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Subnet outside of the VPC", + { + "CidrBlock": "11.0.0.0/16", + }, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [ + ValidationError( + "'10.0.0.0/24' is not a valid subnet of ['11.0.0.0/16']", + rule=VpcSubnetCidr(), + path_override=deque( + ["Resources", "VpcV4Subnet1", "Properties", "CidrBlock"] + ), + ) + ], + ), + ( + "Subnet larger than VPC", + { + "CidrBlock": "10.0.0.0/32", + }, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [ + ValidationError( + "'10.0.0.0/24' is not a valid subnet of ['10.0.0.0/32']", + rule=VpcSubnetCidr(), + path_override=deque( + ["Resources", "VpcV4Subnet1", "Properties", "CidrBlock"] + ), + ) + ], + ), + ( + "Subnet IPV6 CIDR with no ipv6 allocations", + { + "CidrBlock": "11.0.0.0/16", + }, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "Ipv6CidrBlock": "2001:db8::/32", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [ + ValidationError( + ( + "'2001:db8::/32' is specified on a VPC that " + "has no ipv6 networks defined" + ), + rule=VpcSubnetCidr(), + path_override=deque( + ["Resources", "VpcV4Subnet1", "Properties", "Ipv6CidrBlock"] + ), + ) + ], + ), + ( + "Subnet IPV6 CIDR valid with addtional block", + {"CidrBlock": "10.0.0.0/16"}, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcCidr": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "Ipv6CidrBlock": "fc00::/7", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "Ipv6CidrBlock": "fc00::/16", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Invalid subnet IPV6 CIDR valid with addtional block", + {"CidrBlock": "10.0.0.0/32"}, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcCidr": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "Ipv6CidrBlock": "fc00::/32", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "Ipv6CidrBlock": "fc00::/16", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [ + ValidationError( + "'fc00::/16' is not a valid subnet of ['fc00::/32']", + rule=VpcSubnetCidr(), + path_override=deque( + ["Resources", "VpcV4Subnet1", "Properties", "Ipv6CidrBlock"] + ), + ) + ], + ), + ( + "Subnet IPV4 CIDR valid with IPam Pool ID", + {"Ipv4IpamPoolId": "poolid"}, + _template, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Subnet IPV4 CIDR valid with multiple CIDRs", + {"CidrBlock": "10.0.0.0/16"}, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcCidr": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "CidrBlock": "11.0.0.0/16", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "11.0.0.0/24", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Subnet IPV4 CIDR invalid with multiple CIDRs with ipam pool ID", + {"CidrBlock": "10.0.0.0/16"}, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcCidr": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "Ipv4IpamPoolId": "pool-id", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "11.0.0.0/16", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ( + "Subnet IPV6 CIDR invalid with multiple CIDRs and one ipam pool ID", + {"CidrBlock": "10.0.0.0/16"}, + { + "Resources": { + "Vpc": { + "Type": "AWS::EC2::VPC", + }, + "VpcCidr1": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "Ipv6IpamPoolId": "pool-id", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcCidr2": { + "Type": "AWS::EC2::VPCCidrBlock", + "Properties": { + "Ipv6CidrBlock": "fc00::/32", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + "VpcV4Subnet1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "Ipv6CidrBlock": "fc00::/16", + "VpcId": {"Fn::GetAtt": ["Vpc", "VpcId"]}, + }, + }, + }, + }, + {"path": ["Resources", "Vpc", "Properties"]}, + [], + ), + ], + indirect=["template", "path"], +) +def test_validate(name, instance, expected, rule, validator): + errs = list(rule.validate(validator, "", instance, {})) + + assert ( + errs == expected + ), f"Expected test {name!r} to have {expected!r} but got {errs!r}" diff --git a/test/unit/rules/resources/ec2/test_vpc_subnet_overlap.py b/test/unit/rules/resources/ec2/test_vpc_subnet_overlap.py new file mode 100644 index 0000000000..4d3265be54 --- /dev/null +++ b/test/unit/rules/resources/ec2/test_vpc_subnet_overlap.py @@ -0,0 +1,182 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque +from ipaddress import ip_network + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.resources.ectwo.VpcSubnetOverlap import VpcSubnetOverlap + + +@pytest.fixture(scope="module") +def rule(): + rule = VpcSubnetOverlap() + yield rule + + +@pytest.fixture +def template(): + return { + "Parameters": {"MyCidr": {"Type": "String"}}, + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]}, + "IsUsWest2": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-west-2"]}, + "IsUs": { + "Fn::Or": [ + {"Condition": "IsUsEast1"}, + {"Condition": "IsUsWest2"}, + ] + }, + "IsNotUs": {"Fn::Not": [{"Condition": "IsUs"}]}, + }, + "Resources": {}, + } + + +@pytest.mark.parametrize( + "name,instance,starting_subnets,expected", + [ + ( + "Valid with no overlap on ipv4", + {"VpcId": {"Ref": "Vpc"}, "CidrBlock": "10.0.1.0/24"}, + {"Vpc": [(ip_network("10.0.0.0/24"), {})]}, + [], + ), + ( + "Valid with a bad cidr", + {"VpcId": {"Ref": "Vpc"}, "CidrBlock": "10.0.0/24"}, + {"Vpc": [(ip_network("10.0.0.0/24"), {})]}, + [], + ), + ( + "Valid with no overlap on ipv6", + {"VpcId": {"Fn::GetAtt": "Vpc.VpcId"}, "Ipv6CidrBlock": "2001:db8::/32"}, + {"Vpc": [(ip_network("2002:db8::/32"), {})]}, + [], + ), + ( + "Valid with a function", + {"VpcId": {"Ref": "Vpc"}, "CidrBlock": {"Ref": "MyCidr"}}, + {"Vpc": [(ip_network("10.0.0.0/24"), {})]}, + [], + ), + ( + "Valid with no previous setting", + {"VpcId": "vpc-123456", "CidrBlock": "10.0.0.0/24"}, + {}, + [], + ), + ( + "Valid with an invalid function", + {"VpcId": {"DNE": "Vpc"}, "CidrBlock": "10.0.0/24"}, + {"Vpc": [(ip_network("10.0.0.0/24"), {})]}, + [], + ), + ( + "Valid with vpc using another function", + {"VpcId": {"Fn::Join": ["vpc-1"]}, "CidrBlock": "10.0.1.0/24"}, + {"Vpc": [(ip_network("10.0.0.0/24"), {})]}, + [], + ), + ( + "Invalid with a overlap on ipv4", + {"VpcId": {"Ref": "Vpc"}, "CidrBlock": "10.0.0.0/24"}, + {"Vpc": [(ip_network("10.0.0.0/22"), {})]}, + [ + ValidationError( + "'10.0.0.0/24' overlaps with '10.0.0.0/22'", + rule=VpcSubnetOverlap(), + path=deque(["CidrBlock"]), + ) + ], + ), + ( + "Invalid with a overlap on ipv6", + {"VpcId": {"Ref": "Vpc"}, "Ipv6CidrBlock": "fc00::/32"}, + {"Vpc": [(ip_network("fc00::/16"), {})]}, + [ + ValidationError( + "'fc00::/32' overlaps with 'fc00::/16'", + rule=VpcSubnetOverlap(), + path=deque(["Ipv6CidrBlock"]), + ) + ], + ), + ( + "Valid with ipv4 and ipv6", + {"VpcId": {"Ref": "Vpc"}, "Ipv6CidrBlock": "fc01::/32"}, + { + "Vpc": [ + (ip_network("fc00::/32"), {}), + (ip_network("10.0.0.0/24"), {}), + ] + }, + [], + ), + ( + "Valid with conflicting conditions", + { + "VpcId": {"Ref": "Vpc"}, + "CidrBlock": { + "Fn::If": ["IsNotUs", "10.0.0.0/24", {"Ref": "AWS::NoValue"}] + }, + }, + { + "Vpc": [ + ( + ip_network("10.0.0.0/24"), + { + "IsUsEast1": True, + "IsUsWest2": False, + "IsUs": True, + "IsNotUs": False, + }, + ), + ] + }, + [], + ), + ( + "Invalid with conflicting conditions", + { + "VpcId": {"Ref": "Vpc"}, + "CidrBlock": {"Fn::If": ["IsNotUs", "10.0.1.0/24", "10.0.0.0/32"]}, + }, + { + "Vpc": [ + ( + ip_network("10.0.0.0/24"), + { + "IsUsEast1": True, + "IsUsWest2": False, + "IsUs": True, + "IsNotUs": False, + }, + ), + ] + }, + [ + ValidationError( + "'10.0.0.0/32' overlaps with '10.0.0.0/24'", + rule=VpcSubnetOverlap(), + path=deque(["CidrBlock", "Fn::If", 2]), + ) + ], + ), + ], + indirect=[], +) +def test_validate(name, instance, starting_subnets, expected, rule, validator): + + rule._subnets = starting_subnets + errs = list(rule.validate(validator, "", instance, {})) + + for err in errs: + print(err.path) + assert ( + errs == expected + ), f"Expected test {name!r} to have {expected!r} but got {errs!r}"