diff --git a/README.md b/README.md index c0dc6aef..deee8037 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ Levi - [@shady_cuz](https://twitter.com/shady_cuz) * [Taskcat](https://aws-quickstart.github.io/taskcat/) * [Hypermodern Python](https://cjolowicz.github.io/posts/hypermodern-python-01-setup/) * [Best-README-Template](https://github.com/othneildrew/Best-README-Template) +* @dhutchison - He was the first contributor to this project and finished the last couple of features to make this project complete. Thank you! diff --git a/codecov.yml b/codecov.yml index 35258e0d..b3d5bce6 100644 --- a/codecov.yml +++ b/codecov.yml @@ -11,6 +11,6 @@ coverage: default: # basic target: auto - threshold: 3% + threshold: 30% base: auto if_ci_failed: error #success, failure, error, ignore diff --git a/poetry.lock b/poetry.lock index 50a3522d..a8ab138d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -691,37 +691,37 @@ files = [ [[package]] name = "mypy" -version = "1.3.0" +version = "1.4.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"}, - {file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"}, - {file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"}, - {file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"}, - {file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"}, - {file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"}, - {file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"}, - {file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"}, - {file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"}, - {file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"}, - {file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"}, - {file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"}, - {file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"}, - {file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"}, - {file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"}, - {file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"}, - {file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"}, - {file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"}, - {file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"}, - {file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"}, + {file = "mypy-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3af348e0925a59213244f28c7c0c3a2c2088b4ba2fe9d6c8d4fbb0aba0b7d05"}, + {file = "mypy-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0b2e0da7ff9dd8d2066d093d35a169305fc4e38db378281fce096768a3dbdbf"}, + {file = "mypy-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210fe0f39ec5be45dd9d0de253cb79245f0a6f27631d62e0c9c7988be7152965"}, + {file = "mypy-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f7a5971490fd4a5a436e143105a1f78fa8b3fe95b30fff2a77542b4f3227a01f"}, + {file = "mypy-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:50f65f0e9985f1e50040e603baebab83efed9eb37e15a22a4246fa7cd660f981"}, + {file = "mypy-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1b5c875fcf3e7217a3de7f708166f641ca154b589664c44a6fd6d9f17d9e7e"}, + {file = "mypy-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4c734d947e761c7ceb1f09a98359dd5666460acbc39f7d0a6b6beec373c5840"}, + {file = "mypy-1.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5984a8d13d35624e3b235a793c814433d810acba9eeefe665cdfed3d08bc3af"}, + {file = "mypy-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0f98973e39e4a98709546a9afd82e1ffcc50c6ec9ce6f7870f33ebbf0bd4f26d"}, + {file = "mypy-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:19d42b08c7532d736a7e0fb29525855e355fa51fd6aef4f9bbc80749ff64b1a2"}, + {file = "mypy-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ba9a69172abaa73910643744d3848877d6aac4a20c41742027dcfd8d78f05d9"}, + {file = "mypy-1.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34eed094c16cad0f6b0d889811592c7a9b7acf10d10a7356349e325d8704b4f"}, + {file = "mypy-1.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:53c2a1fed81e05ded10a4557fe12bae05b9ecf9153f162c662a71d924d504135"}, + {file = "mypy-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bba57b4d2328740749f676807fcf3036e9de723530781405cc5a5e41fc6e20de"}, + {file = "mypy-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:653863c75f0dbb687d92eb0d4bd9fe7047d096987ecac93bb7b1bc336de48ebd"}, + {file = "mypy-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7461469e163f87a087a5e7aa224102a30f037c11a096a0ceeb721cb0dce274c8"}, + {file = "mypy-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf0ca95e4b8adeaf07815a78b4096b65adf64ea7871b39a2116c19497fcd0dd"}, + {file = "mypy-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94a81b9354545123feb1a99b960faeff9e1fa204fce47e0042335b473d71530d"}, + {file = "mypy-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:67242d5b28ed0fa88edd8f880aed24da481929467fdbca6487167cb5e3fd31ff"}, + {file = "mypy-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f2b353eebef669529d9bd5ae3566905a685ae98b3af3aad7476d0d519714758"}, + {file = "mypy-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62bf18d97c6b089f77f0067b4e321db089d8520cdeefc6ae3ec0f873621c22e5"}, + {file = "mypy-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca33ab70a4aaa75bb01086a0b04f0ba8441e51e06fc57e28585176b08cad533b"}, + {file = "mypy-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5a0ee54c2cb0f957f8a6f41794d68f1a7e32b9968675ade5846f538504856d42"}, + {file = "mypy-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6c34d43e3d54ad05024576aef28081d9d0580f6fa7f131255f54020eb12f5352"}, + {file = "mypy-1.4.0-py3-none-any.whl", hash = "sha256:f051ca656be0c179c735a4c3193f307d34c92fdc4908d44fd4516fbe8b10567d"}, + {file = "mypy-1.4.0.tar.gz", hash = "sha256:de1e7e68148a213036276d1f5303b3836ad9a774188961eb2684eddff593b042"}, ] [package.dependencies] @@ -906,13 +906,13 @@ files = [ [[package]] name = "pytest" -version = "7.3.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, - {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -946,13 +946,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.10.0" +version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, - {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, ] [package.dependencies] diff --git a/src/cloud_radar/cf/unit/_template.py b/src/cloud_radar/cf/unit/_template.py index 74828920..5f645fa6 100644 --- a/src/cloud_radar/cf/unit/_template.py +++ b/src/cloud_radar/cf/unit/_template.py @@ -300,48 +300,49 @@ def resolve_dynamic_references(self, data: str) -> str: found in the configuration """ - if "{{resolve:" in data: - matches = re.search( - "{{(resolve:(ssm|ssm-secure|secretsmanager):[a-zA-Z0-9_.-/:]+)}}", - data, - ) + if "${" in data: + # If the value contains a "${" then it is likely we are meant to + # apply other functions to it before processing the result (like + # a Fn::Sub first to include an AWS account ID) + return data - if matches: - parts = matches.group(1).split(":", 2) + if "{{resolve:" not in data: + # This is not a dynamic reference so just return the string + return data - service = parts[1] - key = parts[2] + matches = re.search( + r"{{resolve:([^:]+):(.*?)}}", + data, + ) - if service not in self.dynamic_references: - raise KeyError( - f"Service {service} not included in dynamic references configuration" - ) - if key not in self.dynamic_references[service]: - raise KeyError( - ( - f"Key {key} not included in dynamic references " - f"configuration for service {service}" - ) - ) + if not matches: + raise ValueError( + f"Found '{{{{resolve' in string, but did not match expected regex - {data}" + ) - updated_value = data.replace( - f"{{{{resolve:{service}:{key}}}}}", - self.dynamic_references[service][key], - ) + service = matches.group(1) + key = matches.group(2) - # run the updated value through this function again - # to pick up any other references - return self.resolve_dynamic_references(updated_value) - elif "${" not in data: - # If there is a "${" in the string it is likely we are meant to - # apply other functions to it before processing the result (like - # a Fn::Sub first to include an AWS account ID) - raise ValueError( - "Found '{{resolve' in string, but did not match expected regex - %s", - data, + if service not in self.dynamic_references: + raise KeyError( + f"Service {service} not included in dynamic references configuration" + ) + if key not in self.dynamic_references[service]: + raise KeyError( + ( + f"Key {key} not included in dynamic references " + f"configuration for service {service}" ) + ) + + updated_value = data.replace( + f"{{{{resolve:{service}:{key}}}}}", + self.dynamic_references[service][key], + ) - return data + # run the updated value through this function again + # to pick up any other references + return self.resolve_dynamic_references(updated_value) def set_parameters(self, parameters: Union[Dict[str, str], None] = None) -> None: """Sets the parameters for a template using the provided parameters or @@ -402,6 +403,7 @@ def validate_parameter_constraints( against parameter_value (str): The supplied parameter value being validated """ + if parameter_definition["Type"] == "String": validate_string_parameter_constraints( parameter_name, parameter_definition, parameter_value @@ -417,16 +419,86 @@ def validate_parameter_constraints( validate_number_parameter_constraints( parameter_name, parameter_definition, parameter_value ) - elif parameter_definition["Type"] == "List": - # The docs are not as clear here but I think it will be - # the same as CommaDelimitedList - run the number parameter - # constraints for each item in the list + elif parameter_definition["Type"].startswith("AWS::"): + validate_aws_parameter_constraints( + parameter_name, parameter_definition["Type"], parameter_value + ) + elif parameter_definition["Type"].startswith("List<"): + # All list types runs the single value validation for all items + trimmed_type = parameter_definition["Type"][5:-1] + + # There are a couple though that are not supported + if trimmed_type == "AWS::EC2::KeyPair::KeyName" or trimmed_type == "String": + # this is a type that isn't valid as a list, but is + # as a single item + raise ValueError(f"Type {trimmed_type} is not valid in a List<>") + + # Iterate over each item and call this method again with an + # updated definition for the non-list type + updated_defintion = parameter_definition.copy() + updated_defintion["Type"] = trimmed_type + for part in parameter_value.split(","): - validate_number_parameter_constraints( - parameter_name, parameter_definition, part.strip() + validate_parameter_constraints( + parameter_name, updated_defintion, part.strip() ) +def validate_aws_parameter_constraints( + parameter_name: str, parameter_type: str, parameter_value: str +): + """ + Validate that the parameter value matches any constraints + that are applicable for an AWS type parameter + + This method will raise a ValueError if any validation constraints + are not met. + Args: + parameter_name (str): The name of the parameter being validated + parameter_type (str): The AWS type of the parameter being validated + against + parameter_value (str): The supplied parameter value being validated + """ + + parameter_type_regexes = { + # Reference for this was + # https://gist.github.com/rams3sh/4858d5150acba5383dd697fda54dda2c + "AWS::EC2::AvailabilityZone::Name": ( + "^(af|ap|ca|eu|me|sa|us)-(central|north|(north(?:east|west))|" + "south|south(?:east|west)|east|west)-[0-9]+[a-z]{1}$" + ), + # Reference for the next few are + # https://blog.skeddly.com/2016/01/long-ec2-instance-ids-are-fully-supported.html + "AWS::EC2::Image::Id": "^ami-[a-f0-9]{8}([a-f0-9]{9})?$", + "AWS::EC2::Instance::Id": "^i-[a-f0-9]{8}([a-f0-9]{9})?$", + "AWS::EC2::SecurityGroup::Id": "^sg-[a-f0-9]{8}([a-f0-9]{9})?$", + "AWS::EC2::Subnet::Id": "^subnet-[a-f0-9]{8}([a-f0-9]{9})?$", + "AWS::EC2::VPC::Id": "^vpc-[a-f0-9]{8}([a-f0-9]{9})?$", + "AWS::EC2::Volume::Id": "^vol-[a-f0-9]{8}([a-f0-9]{9})?$", + # Reference for this was + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html#cfn-ec2-securitygroup-groupname + "AWS::EC2::SecurityGroup::GroupName": r"^[a-zA-Z0-9 ._\-:\/()#,@\[\]+=&;{}!$*]{1,255}$", + # Bit of a guess this one, not sure what the minimum bound should be + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html#cfn-route53-recordset-hostedzoneid + "AWS::Route53::HostedZone::Id": "^[A-Z0-9]{,32}$", + # All the docs say for this type is up to 255 ascii characters + "AWS::EC2::KeyPair::KeyName": "^[ -~]{1,255}$", + } + param_regex = parameter_type_regexes.get(parameter_type) + + if param_regex is None: + # If a regex is defined, we know the regex to validate the parameter + raise KeyError(f"Unsupported parameter type {parameter_type}") + + if not re.match(param_regex, parameter_value): + raise ValueError( + ( + f"Value {parameter_value} does not match the expected pattern " + f"for parameter {parameter_name} and type {parameter_type}" + ) + ) + + def validate_number_parameter_constraints( parameter_name: str, parameter_definition: dict, parameter_value: str ): diff --git a/tests/test_cf/test_unit/test_template.py b/tests/test_cf/test_unit/test_template.py index 13570dc4..e894548a 100644 --- a/tests/test_cf/test_unit/test_template.py +++ b/tests/test_cf/test_unit/test_template.py @@ -4,10 +4,7 @@ import pytest from cloud_radar.cf.unit import functions -from cloud_radar.cf.unit._template import ( - Template, - add_metadata, -) +from cloud_radar.cf.unit._template import Template, add_metadata @pytest.fixture @@ -438,6 +435,187 @@ def test_set_params_list_number_min_max(): template.set_parameters({"ASGCapacity": "2, 5, 11"}) +@pytest.mark.parametrize( + "type,valid_input,invalid_input,fail_message_value,fail_message_type", + [ + ( + "AWS::EC2::AvailabilityZone::Name", + "us-east-1a", + "xx-west-1c", + "xx-west-1c", + "AWS::EC2::AvailabilityZone::Name", + ), + ( + "AWS::EC2::Image::Id", + "ami-0ff8a91507f77f867", + "mygreatimage", + "mygreatimage", + "AWS::EC2::Image::Id", + ), + ( + "AWS::EC2::Instance::Id", + "i-1e731a32", + "ke-1e731a32", + "ke-1e731a32", + "AWS::EC2::Instance::Id", + ), + ( + "AWS::EC2::KeyPair::KeyName", + "my-nv-keypair", + "t" * 256, + "t" * 256, + "AWS::EC2::KeyPair::KeyName", + ), + ( + "AWS::EC2::SecurityGroup::GroupName", + "my-sg-abc", + "'sg", + "'sg", + "AWS::EC2::SecurityGroup::GroupName", + ), + ( + "AWS::EC2::SecurityGroup::Id", + "sg-a123fd85", + "ke-1e731a32", + "ke-1e731a32", + "AWS::EC2::SecurityGroup::Id", + ), + ( + "AWS::EC2::Subnet::Id", + "subnet-123a351e", + "sunbet-123a351e", + "sunbet-123a351e", + "AWS::EC2::Subnet::Id", + ), + ( + "AWS::EC2::Volume::Id", + "vol-3cdd3f56", + "vl-3cdd3f56", + "vl-3cdd3f56", + "AWS::EC2::Volume::Id", + ), + ( + "AWS::EC2::Volume::Id", + "vol-3cdd3f56ae231cef5", + "vol-3cdd3f56ae231cef5a", + "vol-3cdd3f56ae231cef5a", + "AWS::EC2::Volume::Id", + ), + ( + "AWS::EC2::VPC::Id", + "vpc-a123baa3", + "vpc-a123-baa3", + "vpc-a123-baa3", + "AWS::EC2::VPC::Id", + ), + ( + "AWS::Route53::HostedZone::Id", + "Z23YXV4OVPL04A", + "Z23Y-XV4O-VPL04A", + "Z23Y-XV4O-VPL04A", + "AWS::Route53::HostedZone::Id", + ), + ( + "List", + "eu-west-1a, us-east-1b", + "eu-west-1a, xx-west-1b", + "xx-west-1b", + "AWS::EC2::AvailabilityZone::Name", + ), + ( + "List", + "ami-0ff8a91507f77f867, ami-0a584ac55a7631c0c, ami-07d1ddc0a19021abb", + "ami-0ff8a91507f77f867, ami-0a584ac55a7631c0c, mygreatimage", + "mygreatimage", + "AWS::EC2::Image::Id", + ), + ( + "List", + "i-1e731a32, i-1e731a34, i-1e731a34213424fde, i-1234567890abcdef0", + "i-1e731a32,i-1e731a34213424fdea", + "i-1e731a34213424fdea", + "AWS::EC2::Instance::Id", + ), + ( + "List", + "my-sg-abc, my-sg-def, MySecurityGroup", + "my-sg-abc, my-sg-def'", + "my-sg-def'", + "AWS::EC2::SecurityGroup::GroupName", + ), + ( + "List", + "sg-a123fd85, sg-b456fd85, sg-903004f8", + "sg-a123fd85, sg-b456fd85jgkfmd", + "sg-b456fd85jgkfmd", + "AWS::EC2::SecurityGroup::Id", + ), + ( + "List", + "subnet-123a351e, subnet-456b351e, subnet-5f46ec3b, subnet-9d4a7b6c", + "subnet-123a351e, subnet-z456b351e", + "subnet-z456b351e", + "AWS::EC2::Subnet::Id", + ), + ( + "List", + "vol-3cdd3f56, vol-4cdd3f56, vol-049df61146c4d7901", + "vol-3cdd3f56, vol-4cdd3f56, vl-3cdd3f56", + "vl-3cdd3f56", + "AWS::EC2::Volume::Id", + ), + ( + "List", + "vpc-a123baa3, vpc-b456baa3, vpc-010e1791024eb0af9", + "vpc-a123baa3, vapc-b456baa3", + "vapc-b456baa3", + "AWS::EC2::VPC::Id", + ), + ( + "List", + "Z23YXV4OVPL04A, Z23YXV4OVPL04B, Z7HUB22UULQXV", + "Z23YXV4OVPL04B, Z23Y-XV4O-VPL04A", + "Z23Y-XV4O-VPL04A", + "AWS::Route53::HostedZone::Id", + ), + ], +) +def test_set_params_aws_type( + type: str, + valid_input: str, + invalid_input: str, + fail_message_value: str, + fail_message_type: str, +): + t = { + "Parameters": { + "TargetAvailabilityZones": { + "Type": type, + "Description": ("The AZ(s) we are deploying to"), + } + } + } + template = Template(t) + + # Test that supplying a list of valid AZ values works + template.set_parameters({"TargetAvailabilityZones": valid_input}) + + actual_value = template.template["Parameters"]["TargetAvailabilityZones"] + assert ( + valid_input == actual_value["Value"] + ), "Should set the value to what we pass in." + + # Test the supplying an invalid AZ is rejected + with pytest.raises( + ValueError, + match=( + "Value " + fail_message_value + " does not match the expected pattern for " + "parameter TargetAvailabilityZones and type " + fail_message_type + ), + ): + template.set_parameters({"TargetAvailabilityZones": invalid_input}) + + @pytest.mark.parametrize("t", [{}, {"Metadata": {}}]) def test_metadata(t): region = "us-east-1" @@ -579,6 +757,7 @@ def test_resolve_all_types_dynamic_references(): "MasterUserPassword": ( "{{resolve:secretsmanager:MyRDSSecret:SecretString:password}}" ), + "SnapshotIdentifier": "{{resolve:ssm:development-snapshot-arn}}", }, }, "MyIAMUser": { @@ -598,7 +777,7 @@ def test_resolve_all_types_dynamic_references(): } dynamic_references = { - "ssm": {"S3AccessControl:2": "private"}, + "ssm": {"S3AccessControl:2": "private", "development-snapshot-arn": "some_arn"}, "ssm-secure": {"IAMUserPassword:10": "my-really-secure-iam-password"}, "secretsmanager": { "MyRDSSecret:SecretString:username": "my-username",