diff --git a/examples/unit/naming/README.md b/examples/unit/naming/README.md index 546f24c..0df97d4 100644 --- a/examples/unit/naming/README.md +++ b/examples/unit/naming/README.md @@ -62,59 +62,3 @@ If a resource was incorrectly named and did not match the expected pattern, this ``` E AssertionError: Resource 'rFileSystem' tag 'Name' value 'my-test-xx-west-3-vol' did not match expected pattern '^[a-z]*-vol$'. ``` - - -# Advanced Naming Convention Checks - -With the previous examples you can easily target individual resources to check their names meet expectations, but adding a test like this for every single resource in templates across your organisation is monotonous and time consuming. It is common for organisations to define conventions like all S3 buckets should have a naming convention like `{company name}-{AWS region}-{some name}`. This example covers the more advanced case for doing resource type to pattern conventions. This example is taken from the `test_naming_conventions` method of [test_naming_advanced.py](./test_naming_advanced.py). - - -```python - # Ideally this dict would be coming from a common library - # function that you have shared between all your test cases - # (assuming consistency is the goal). - # - # This defines all the resource types we want to check, the pattern to match, - # and either the details of the Tag or Property that the name is held in. - type_patterns = { - "AWS::EFS::FileSystem": { - "Tag": "Name", - # This TagProperty is optional. The default is 'Tags', - # but some resources use a different property name for - # their tags. - "TagProperty": "FileSystemTags", - "Pattern": r"^[a-z0-9-]*-vol$", - }, - "AWS::S3::Bucket": { - "Property": "BucketName", - "Pattern": r"^[a-z0-9-]*-xx-west-3[a-z0-9-]*$", - }, - "AWS::S3::BucketPolicy": { - # The BucketPolicy type does not support custom names. If we do not - # want to set fail_on_missing_type=False when we call the assertion - # below then we need to include this type in the dict, and set it - # not to be checked. - # This approach ensures that types do not slip through unintentionally. - "Check": False - }, - } - - stack.assert_resource_type_property_value_conventions(type_patterns) -``` - -In this example our object containing our naming conventions includes three types, showing the different types of configuration that can be used: - -1. `AWS::EFS::FileSystem` - A resource type which holds it's name in the tag `Name`, using a non-standard tag property `FileSystemTags` -2. `AWS::S3::Bucket` - A resource type which holds it's name in a property called `BucketName` -3. `AWS::S3::BucketPolicy` - A resource type which does not have a custom name, so is configured not to perform any pattern checking. This is required to be configured unless `fail_on_missing_type=False` is supplied to the `assert_resource_type_property_value_conventions` method. - - -If a resource is encountered that did not match the defined pattern, you would get a test failure with an error like this: -``` -E AssertionError: Resource 'rFileSystem' tag 'Name' value 'my-test-xx-west-3-vol' did not match expected pattern '^[a-z0-9-]*-FAIL$'. -``` - -The default behaviour is for `fail_on_missing_type` to be True, so if a resource is encountered with a type not defined in `type_patterns`, an assertion error like the following will be raised. -``` -E AssertionError: Resource 'rFileSystem' has type 'AWS::EFS::FileSystem' which is not included in the supplied type_patterns. -``` diff --git a/examples/unit/naming/test_naming_advanced.py b/examples/unit/naming/test_naming_advanced.py deleted file mode 100644 index 2593f0b..0000000 --- a/examples/unit/naming/test_naming_advanced.py +++ /dev/null @@ -1,123 +0,0 @@ -import re -from pathlib import Path - -import pytest - -from cloud_radar.cf.unit import Stack, Template - - -@pytest.fixture -def stack(): - template_path = Path(__file__).parent / "naming_resources.yaml" - template = Template.from_yaml(template_path) - - # In these cases I'll usually use a non-existant region to ensure a real region - # is not hard coded - return template.create_stack(params={"pName": "test"}, region="xx-west-3") - - -def test_naming_conventions(stack: Stack): - """ - This test shows how common naming conventions can be checked across all - resources in a stack, including ignoring resource types which do not support - names. - - Args: - stack (Stack): the stack being used for these test examples - """ - - # Ideally this dict would be coming from a common library - # function that you have shared between all your test cases - # (assuming consistency is the goal). - # - # This defines all the resource types we want to check, the pattern to match, - # and either the details of the Tag or Property that the name is held in. - type_patterns = { - "AWS::EFS::FileSystem": { - "Tag": "Name", - # This TagProperty is optional. The default is 'Tags', - # but some resources use a different property name for - # their tags. - "TagProperty": "FileSystemTags", - "Pattern": r"^[a-z0-9-]*-vol$", - }, - "AWS::S3::Bucket": { - "Property": "BucketName", - "Pattern": r"^[a-z0-9-]*-xx-west-3[a-z0-9-]*$", - }, - "AWS::S3::BucketPolicy": { - # The BucketPolicy type does not support custom names. If we do not - # want to set fail_on_missing_type=False when we call the assertion - # below then we need to include this type in the dict, and set it - # not to be checked. - # This approach ensures that types do not slip through unintentionally. - "Check": False - }, - } - - stack.assert_resource_type_property_value_conventions(type_patterns) - - -def test_not_matching_pattern(stack: Stack): - """ - This test validates that if a resource is encountered that does not match the - defined pattern, the correct assertion error is raised. - - Args: - stack (Stack): the stack being used for these test examples - """ - - type_patterns = { - "AWS::EFS::FileSystem": { - "Tag": "Name", - "TagProperty": "FileSystemTags", - "Pattern": r"^[a-z0-9-]*-FAIL$", - }, - "AWS::S3::Bucket": { - "Property": "BucketName", - "Pattern": r"^[a-z0-9-]*-xx-west-3[a-z0-9-]*$", - }, - } - - with pytest.raises( - AssertionError, - match=re.escape( - ( - "Resource 'rFileSystem' tag 'Name' value 'my-test-xx-west-3-vol' " - "did not match expected pattern '^[a-z0-9-]*-FAIL$'." - ) - ), - ): - stack.assert_resource_type_property_value_conventions( - type_patterns, fail_on_missing_type=False - ) - - -def test_missing_convention(stack: Stack): - """ - This test shows how the fail_on_missing_type parameter on - assert_resource_type_property_value_conventions can be used to - error or ignore types without a convention defined. - - Args: - stack (Stack): the stack being used for these test examples - """ - - # There is an optional parameter that can be used to ignore a resource being found - # that does not have a convention set for it. - # With this set, if we supplied a dict without any entries then no assertion error - # would be raised. - stack.assert_resource_type_property_value_conventions( - type_patterns={}, fail_on_missing_type=False - ) - - # The default behaviour is for fail_on_missing_type to be set to True, - # resulting in an assertion error - with pytest.raises( - AssertionError, - match=( - "Resource 'rFileSystem' has type 'AWS::EFS::FileSystem' " - "which is not included in the supplied type_patterns." - ), - ): - stack.assert_resource_type_property_value_conventions(type_patterns={}) diff --git a/src/cloud_radar/cf/unit/_stack.py b/src/cloud_radar/cf/unit/_stack.py index 29b5e7d..8ae1953 100644 --- a/src/cloud_radar/cf/unit/_stack.py +++ b/src/cloud_radar/cf/unit/_stack.py @@ -189,105 +189,3 @@ def get_output(self, output_name: str): output = Output(output_name, output_data) return output - - def assert_resource_type_property_value_conventions( - self, type_patterns: dict, fail_on_missing_type: bool = True - ): - """ - This method will perform assertions on all resources in the stack to check - that a property or tag value matches a regex pattern. - - This is commonly used to ensure that resources match naming conventions. The - test_naming_advanced.py example file shows how to use this. - - The type_patterns dict uses CloudFormation resource types as the keys, - with the values being an object defining if it should be checked (default - True), the pattern to compare against, and the details of the Property - or Tag to get the value for. - - For a type which uses a property: - { - "AWS::S3::Bucket": { - "Property": "BucketName", - "Pattern": r"^[a-z0-9-]*-xx-west-3[a-z0-9-]*$", - } - } - - For a type which uses a tag: - { - "AWS::EFS::FileSystem": { - "Tag": "Name", - # This TagProperty is optional. The default is 'Tags', - # but some resources use a different property name for - # their tags. - "TagProperty": "FileSystemTags", - "Pattern": r"^[a-z0-9-]*-vol$", - } - } - - For a type which should not be checked: - { - "AWS::S3::BucketPolicy": { - # The BucketPolicy type does not support custom names. If we do not - # want to set fail_on_missing_type=False when we call the assertion - # below then we need to include this type in the dict, and set it - # not to be checked. - # This approach ensures that types do not slip through unintentionally. - "Check": False - } - } - - - Args: - type_patterns (dict): A dict with the key being the resource type, - and the value an object, in one of the formats - specified above. - fail_on_missing_type (bool): If this is set to true, an assertion error - will be thrown if the stack contains a - resource of a type that is not defined in - the type_patterns dict. - """ - - for resource_name in self.data.get("Resources", {}): - resource_value = self.get_resource(resource_name) - - naming_convention = type_patterns.get(resource_value.get("Type")) - resource_type = resource_value.get_type_value() - - assert naming_convention is not None or not fail_on_missing_type, ( - f"Resource '{resource_value.name}' has type '{resource_type}' " - f"which is not included in the supplied type_patterns." - ) - - if naming_convention is not None and naming_convention.get("Check", True): - # Only proceed further if this type is to be checked. - # This might be set to False where a resource cannot have - # a custom name - - assert "Pattern" in naming_convention, ( - f"Naming convention definition for {resource_type} did not contain" - " a 'Pattern', and 'Check' was not set to False." - ) - pattern = naming_convention["Pattern"] - - if "Property" in naming_convention: - # The name is held in a top level property - resource_value.assert_property_value_matches_pattern( - naming_convention["Property"], pattern - ) - elif "Tag" in naming_convention: - # The name is held in a tag. We can also look in the configuration - # for a custom tag property, as not all resources use Tag - resource_value.assert_tag_value_matches_pattern( - naming_convention["Tag"], - pattern, - naming_convention.get("TagProperty", "Tags"), - ) - else: - raise AssertionError( - ( - f"Naming convention definition for {resource_type} did not " - f"contain one of 'Property' or 'Tag', and 'Check' was not " - "set to False." - ) - )