diff --git a/src/cfnlint/data/schemas/other/metadata/configuration.json b/src/cfnlint/data/schemas/other/metadata/configuration.json index 5d8b660460..de09d0d81a 100644 --- a/src/cfnlint/data/schemas/other/metadata/configuration.json +++ b/src/cfnlint/data/schemas/other/metadata/configuration.json @@ -20,12 +20,8 @@ } }, "properties": { - "AWS::CloudFormation::Interface": { - "awsType": "CfnMetadataInterface" - }, - "cfn-lint": { - "awsType": "CfnMetadataCfnLint" - } + "AWS::CloudFormation::Interface": {}, + "cfn-lint": {} }, "type": "object" } diff --git a/src/cfnlint/data/schemas/other/outputs/configuration.json b/src/cfnlint/data/schemas/other/outputs/configuration.json index 259ddd5fb6..1560f32211 100644 --- a/src/cfnlint/data/schemas/other/outputs/configuration.json +++ b/src/cfnlint/data/schemas/other/outputs/configuration.json @@ -18,9 +18,6 @@ "additionalProperties": false, "properties": { "Condition": { - "awsType": [ - "CfnCondition" - ], "type": [ "string" ] diff --git a/src/cfnlint/data/schemas/other/resources/configuration.json b/src/cfnlint/data/schemas/other/resources/configuration.json index 7180d9dc39..1de0385daf 100644 --- a/src/cfnlint/data/schemas/other/resources/configuration.json +++ b/src/cfnlint/data/schemas/other/resources/configuration.json @@ -1,42 +1,8 @@ { - "additionalProperties": { - "$ref": "#/definitions/ResourceConfiguration" - }, + "additionalProperties": false, "definitions": { - "CloudFormationInitConfig": { - "properties": { - "config": { - "properties": { - "commands": { - "awsType": "CfnInitCommands" - }, - "files": { - "awsType": "CfnInitFiles" - }, - "groups": { - "awsType": "CfnInitGroups" - }, - "packages": { - "awsType": "CfnInitPackages" - }, - "services": { - "awsType": "CfnInitServices" - }, - "sources": { - "awsType": "CfnInitSources" - }, - "users": { - "awsType": "CfnInitUsers" - } - }, - "type": "object" - } - }, - "type": "object" - }, "ResourceConfiguration": { "additionalProperties": false, - "awsType": "CfnResourceProperties", "else": { "not": { "required": [ @@ -61,15 +27,12 @@ }, "properties": { "Condition": { - "awsType": "CfnCondition", "type": "string" }, "CreationPolicy": { "type": "object" }, - "DeletionPolicy": { - "awsType": "CfnDeletionPolicy" - }, + "DeletionPolicy": {}, "DependsOn": { "items": { "type": "string" @@ -79,19 +42,13 @@ "array" ] }, - "Metadata": { - "awsType": "CfnResourceMetadata" - }, + "Metadata": {}, "Properties": {}, "Type": { "type": "string" }, - "UpdatePolicy": { - "type": "object" - }, - "UpdateReplacePolicy": { - "awsType": "CfnResourceUpdateReplacePolicy" - }, + "UpdatePolicy": {}, + "UpdateReplacePolicy": {}, "Version": { "type": "string" } @@ -100,13 +57,9 @@ "Type" ], "then": { - "not": { - "required": [ - "CreationPolicy", - "UpdatePolicy" - ] - }, "properties": { + "CreationPolicy": false, + "UpdatePolicy": false, "Version": { "type": "string" } @@ -116,9 +69,13 @@ } }, "maxProperties": 500, + "patternProperties": { + "^[a-zA-Z0-9]+$": { + "$ref": "#/definitions/ResourceConfiguration" + } + }, "propertyNames": { - "maxLength": 255, - "pattern": "^[a-zA-Z0-9]+$" + "maxLength": 255 }, "type": "object" } diff --git a/src/cfnlint/data/schemas/other/resources/metadata.json b/src/cfnlint/data/schemas/other/resources/metadata.json index 5e2620129c..d4d8acb3ef 100644 --- a/src/cfnlint/data/schemas/other/resources/metadata.json +++ b/src/cfnlint/data/schemas/other/resources/metadata.json @@ -1,47 +1,19 @@ { - "allOf": [ - { - "type": [ - "object", - "array" - ] - }, - { + "additionalProperties": { + "type": [ + "object", + "string" + ] + }, + "properties": { + "AWS::CloudFormation::Init": { "additionalProperties": true, - "properties": { - "AWS::CloudFormation::Init": { - "properties": { - "config": { - "properties": { - "commands": { - "awsType": "CfnInitCommands" - }, - "files": { - "awsType": "CfnInitFiles" - }, - "groups": { - "awsType": "CfnInitGroups" - }, - "packages": { - "awsType": "CfnInitPackages" - }, - "services": { - "awsType": "CfnInitServices" - }, - "sources": { - "awsType": "CfnInitSources" - }, - "users": { - "awsType": "CfnInitUsers" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, + "properties": {}, "type": "object" } + }, + "type": [ + "object", + "array" ] } diff --git a/src/cfnlint/data/schemas/other/template/configuration.json b/src/cfnlint/data/schemas/other/template/configuration.json index a26acb8334..4d927c7896 100644 --- a/src/cfnlint/data/schemas/other/template/configuration.json +++ b/src/cfnlint/data/schemas/other/template/configuration.json @@ -16,21 +16,11 @@ "Description": { "type": "string" }, - "Mappings": { - "awsType": "CfnMappings" - }, - "Metadata": { - "awsType": "CfnMetadata" - }, - "Outputs": { - "awsType": "CfnOutputs" - }, - "Parameters": { - "awsType": "CfnParameters" - }, - "Resources": { - "awsType": "CfnResources" - }, + "Mappings": {}, + "Metadata": {}, + "Outputs": {}, + "Parameters": {}, + "Resources": {}, "Rules": { "type": "object" }, diff --git a/src/cfnlint/jsonschema/_keywords.py b/src/cfnlint/jsonschema/_keywords.py index d460998d91..35254ad34b 100644 --- a/src/cfnlint/jsonschema/_keywords.py +++ b/src/cfnlint/jsonschema/_keywords.py @@ -491,7 +491,7 @@ def properties( subschema, path=k[0] if len(k) > 0 else p, schema_path=p, - property_path=k[0] if len(k) > 0 else p, + property_path=p, ) diff --git a/src/cfnlint/rules/_Rule.py b/src/cfnlint/rules/_Rule.py index 26e61a4188..d37a7cd954 100644 --- a/src/cfnlint/rules/_Rule.py +++ b/src/cfnlint/rules/_Rule.py @@ -219,6 +219,8 @@ def __repr__(self): return f"{self.id}: {self.shortdesc}" def __eq__(self, other): + if other is None: + return False return self.id == other.id @property diff --git a/src/cfnlint/rules/conditions/Condition.py b/src/cfnlint/rules/conditions/Condition.py index 168b1b45d9..a80d92d6ee 100644 --- a/src/cfnlint/rules/conditions/Condition.py +++ b/src/cfnlint/rules/conditions/Condition.py @@ -18,11 +18,11 @@ class Condition(BaseFn): tags = ["functions", "and"] def __init__(self) -> None: - super().__init__("Fn::And", ("boolean",)) + super().__init__("Condition", ("boolean",)) self.condition = self.validate def schema(self, validator, instance) -> Dict[str, Any]: return { "type": "string", - "awsType": "CfnCondition", + "enum": list(validator.context.conditions.keys()), } diff --git a/src/cfnlint/rules/jsonschema/AwsType.py b/src/cfnlint/rules/jsonschema/AwsType.py deleted file mode 100644 index b4c058a40d..0000000000 --- a/src/cfnlint/rules/jsonschema/AwsType.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 -""" - -from cfnlint.jsonschema._utils import ensure_list -from cfnlint.rules import CloudFormationLintRule - - -class AwsType(CloudFormationLintRule): - """Check Conditions awsType values are correct""" - - id = "E1100" - shortdesc = "Validate awsType(s) from the JSON schema" - description = "This rule holds all the awsTypes that can be used in " "JSON schema" - source_url = "https://github.com/aws-cloudformation/cfn-python-lint" - tags = ["base"] - - def __init__(self) -> None: - super().__init__() - self.types = { - "CfnCondition": "E8002", - "CfnDynamicReferenceSecret": "E1051", - "CfnResources": "E3001", - "CfnResourceProperties": "E3002", - } - self.child_rules = dict.fromkeys(list(self.types.values())) - - # pylint: disable=unused-argument - def awsType(self, validator, tS, instance, schema): - tS = ensure_list(tS) - for t in tS: - rule = self.child_rules.get(self.types.get(t, "")) - if not rule: - return - - if hasattr(rule, t.lower()) and callable(getattr(rule, t.lower())): - fn = getattr(rule, t.lower()) - yield from fn(validator, t, instance, schema) - else: - raise ValueError(f"{t.lower()!r} not found in ${rule.id!r}") diff --git a/src/cfnlint/rules/jsonschema/CfnLint.py b/src/cfnlint/rules/jsonschema/CfnLint.py index 33e30875f1..65f1199021 100644 --- a/src/cfnlint/rules/jsonschema/CfnLint.py +++ b/src/cfnlint/rules/jsonschema/CfnLint.py @@ -3,6 +3,7 @@ SPDX-License-Identifier: MIT-0 """ +from cfnlint.jsonschema._utils import Unset from cfnlint.rules import CloudFormationLintRule @@ -19,6 +20,7 @@ def cfnLint(self, validator, keywords, instance, schema): add_cfn_lint_keyword=False, ) ) + for keyword in keywords: for rule in self.child_rules.values(): if rule is None: @@ -30,5 +32,6 @@ def cfnLint(self, validator, keywords, instance, schema): if rule_keyword == keyword: for err in rule.validate(validator, keyword, instance, schema): if err.rule is None: - err.rule = rule + if isinstance(err.validator, Unset): + err.rule = rule yield err diff --git a/src/cfnlint/rules/jsonschema/CfnLintJsonSchema.py b/src/cfnlint/rules/jsonschema/CfnLintJsonSchema.py index c9c4be702b..31f9e002d7 100644 --- a/src/cfnlint/rules/jsonschema/CfnLintJsonSchema.py +++ b/src/cfnlint/rules/jsonschema/CfnLintJsonSchema.py @@ -10,7 +10,6 @@ from cfnlint.helpers import load_resource from cfnlint.jsonschema import ValidationError -from cfnlint.jsonschema._keywords import type from cfnlint.jsonschema.exceptions import best_match from cfnlint.rules.jsonschema.Base import BaseJsonSchema @@ -38,8 +37,6 @@ def __init__( ) self._use_schema_arg = False - self.validators["type"] = type - @property def schema(self): return self._schema @@ -70,7 +67,10 @@ def validate(self, validator, keywords, instance, schema): cfn_validator = self.extend_validator( validator=validator, schema=schema, - context=validator.context.evolve(functions=[]), + context=validator.context.evolve( + functions=[], + strict_types=True, + ), ) yield from self._iter_errors(cfn_validator, instance) diff --git a/src/cfnlint/rules/jsonschema/JsonSchema.py b/src/cfnlint/rules/jsonschema/JsonSchema.py index f963888d54..8f57288609 100644 --- a/src/cfnlint/rules/jsonschema/JsonSchema.py +++ b/src/cfnlint/rules/jsonschema/JsonSchema.py @@ -7,6 +7,7 @@ from cfnlint.data.schemas.other import template as schema_template from cfnlint.helpers import load_resource from cfnlint.jsonschema import CfnTemplateValidator +from cfnlint.jsonschema._keywords_cfn import cfn_type from cfnlint.rules.jsonschema.Base import BaseJsonSchema from cfnlint.template import Template @@ -27,7 +28,6 @@ def __init__(self): """Init""" super().__init__() self.rule_set = { - "awsType": "E1100", "cfnLint": "E1101", "condition": "E8007", "dynamicReference": "E1050", @@ -55,7 +55,7 @@ def __init__(self): self.config_definition = {"sections": {"default": "", "type": "string"}} self.configure() self.validators = { - "awsType": None, + "type": cfn_type, } @property diff --git a/src/cfnlint/rules/outputs/Condition.py b/src/cfnlint/rules/outputs/Condition.py new file mode 100644 index 0000000000..8bb0ae1037 --- /dev/null +++ b/src/cfnlint/rules/outputs/Condition.py @@ -0,0 +1,38 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from typing import Any + +from cfnlint.jsonschema import Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema + + +class Condition(CfnLintJsonSchema): + + id = "E6005" + shortdesc = "Validate the Output condition is valid" + description = ( + "Check the condition of an output to make sure it exists inside the template" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html" + tags = ["outputs", "conditions"] + + def __init__(self) -> None: + super().__init__( + keywords=["Outputs/*/Condition"], + all_matches=True, + ) + + def validate(self, validator: Validator, _, instance: Any, schema): + if not validator.is_type(instance, "string"): + return + + validator = self.extend_validator( + validator=validator, + schema={"enum": list(validator.context.conditions.keys())}, + context=validator.context.evolve(), + ) + + yield from self._iter_errors(validator, instance) diff --git a/src/cfnlint/rules/resources/Condition.py b/src/cfnlint/rules/resources/Condition.py new file mode 100644 index 0000000000..c2b96aacf9 --- /dev/null +++ b/src/cfnlint/rules/resources/Condition.py @@ -0,0 +1,38 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from typing import Any + +from cfnlint.jsonschema import Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema + + +class Condition(CfnLintJsonSchema): + + id = "E3015" + shortdesc = "Validate the resource condition is valid" + description = ( + "Check the condition of a resource to make sure it exists inside the template" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html" + tags = ["resources", "conditions"] + + def __init__(self) -> None: + super().__init__( + keywords=["Resources/*/Condition"], + all_matches=True, + ) + + def validate(self, validator: Validator, _, instance: Any, schema): + if not validator.is_type(instance, "string"): + return + + validator = self.extend_validator( + validator=validator, + schema={"enum": list(validator.context.conditions.keys())}, + context=validator.context.evolve(), + ) + + yield from self._iter_errors(validator, instance) diff --git a/src/cfnlint/rules/resources/Configuration.py b/src/cfnlint/rules/resources/Configuration.py index b03e54c349..10a5ba4106 100644 --- a/src/cfnlint/rules/resources/Configuration.py +++ b/src/cfnlint/rules/resources/Configuration.py @@ -3,12 +3,16 @@ SPDX-License-Identifier: MIT-0 """ +from typing import Any + +import cfnlint.data.schemas.other.resources import cfnlint.helpers -from cfnlint.data.schemas.other import resources -from cfnlint.rules.jsonschema.Base import BaseJsonSchema +from cfnlint.jsonschema import ValidationResult, Validator +from cfnlint.jsonschema._keywords import patternProperties +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails -class Configuration(BaseJsonSchema): +class Configuration(CfnLintJsonSchema): """Check Base Resource Configuration""" id = "E3001" @@ -20,19 +24,46 @@ class Configuration(BaseJsonSchema): tags = ["resources"] def __init__(self): - super().__init__() + super().__init__( + keywords=["Resources"], + schema_details=SchemaDetails( + cfnlint.data.schemas.other.resources, "configuration.json" + ), + all_matches=True, + ) self.validators = { "maxProperties": None, "propertyNames": None, + "patternProperties": self._pattern_properties, } self.rule_set = { "maxProperties": "E3010", "propertyNames": "E3011", } self.child_rules = dict.fromkeys(list(self.rule_set.values())) - self._schema = cfnlint.helpers.load_resource(resources, "configuration.json") - self.cfnresources = self.validate - @property - def schema(self): - return self._schema + def _pattern_properties( + self, validator: Validator, aP: Any, instance: Any, schema: Any + ): + # We have to rework pattern properties + # to re-add the keyword or we will have an + # infinite loop + validator = validator.evolve( + function_filter=validator.function_filter.evolve( + add_cfn_lint_keyword=True, + ) + ) + + yield from patternProperties(validator, aP, instance, schema) + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: Any + ) -> ValidationResult: + + cfn_validator = self.extend_validator( + validator=validator, + schema=self._schema, + context=validator.context.evolve(), + ) + + yield from self._iter_errors(cfn_validator, instance) diff --git a/src/cfnlint/rules/resources/Metadata.py b/src/cfnlint/rules/resources/Metadata.py index 50aa6ee2c0..0a94d3aec7 100644 --- a/src/cfnlint/rules/resources/Metadata.py +++ b/src/cfnlint/rules/resources/Metadata.py @@ -7,7 +7,8 @@ import cfnlint.data.schemas.other.resources import cfnlint.helpers -from cfnlint.jsonschema import Validator +from cfnlint.jsonschema import ValidationResult, Validator +from cfnlint.jsonschema._keywords import additionalProperties, properties from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails @@ -30,12 +31,45 @@ def __init__(self): ), all_matches=True, ) + self.validators["properties"] = self._properties + self.validators["additionalProperties"] = self._additional_properties - def validate(self, validator: Validator, keywords: Any, instance: Any, schema: Any): + def _properties(self, validator: Validator, pP: Any, instance: Any, schema: Any): + # We have to rework pattern properties + # to re-add the keyword or we will have an + # infinite loop validator = validator.evolve( + function_filter=validator.function_filter.evolve( + add_cfn_lint_keyword=True, + ) + ) + + yield from properties(validator, pP, instance, schema) + + def _additional_properties( + self, validator: Validator, pP: Any, instance: Any, schema: Any + ): + # We have to rework pattern properties + # to re-add the keyword or we will have an + # infinite loop + validator = validator.evolve( + function_filter=validator.function_filter.evolve( + add_cfn_lint_keyword=True, + ) + ) + + yield from additionalProperties(validator, pP, instance, schema) + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: Any + ) -> ValidationResult: + validator = self.extend_validator( + validator=validator, schema=self._schema, context=validator.context.evolve( functions=cfnlint.helpers.FUNCTIONS, + strict_types=False, ), ) + yield from self._iter_errors(validator, instance) diff --git a/src/cfnlint/rules/resources/lmbd/SnapStart.py b/src/cfnlint/rules/resources/lmbd/SnapStart.py index 6de6f599e1..8a1f751cac 100644 --- a/src/cfnlint/rules/resources/lmbd/SnapStart.py +++ b/src/cfnlint/rules/resources/lmbd/SnapStart.py @@ -31,7 +31,7 @@ def validate(self, validator, _, instance, schema): if instance != "PublishedVersions": return - resource_name = validator.context.path[1] + resource_name = validator.context.path.path[1] lambda_version_type = "AWS::Lambda::Version" if list( validator.cfn.get_resource_children(resource_name, [lambda_version_type]) diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py index 135198f392..8da182943d 100644 --- a/src/cfnlint/rules/resources/properties/Properties.py +++ b/src/cfnlint/rules/resources/properties/Properties.py @@ -8,14 +8,14 @@ from typing import Any from cfnlint.helpers import FUNCTIONS, REGION_PRIMARY -from cfnlint.jsonschema import Validator -from cfnlint.rules.jsonschema.Base import BaseJsonSchema +from cfnlint.jsonschema import ValidationResult, Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema from cfnlint.schema.manager import PROVIDER_SCHEMA_MANAGER, ResourceNotFoundError LOGGER = logging.getLogger(__name__) -class Properties(BaseJsonSchema): +class Properties(CfnLintJsonSchema): """Check Base Resource Configuration""" id = "E3002" @@ -26,7 +26,10 @@ class Properties(BaseJsonSchema): def __init__(self): """Init""" - super().__init__() + super().__init__( + keywords=["Resources/*"], + all_matches=True, + ) self.rule_set = { "additionalProperties": "E3002", "anyOf": "E3017", @@ -54,17 +57,21 @@ def __init__(self): } self.child_rules = dict.fromkeys(list(self.rule_set.values())) - def validate(self, validator: Validator, _, instance: Any, schema): + def _validate_resource(self, validator: Validator, _, instance: Any, schema): validator = self.extend_validator(validator, schema, validator.context.evolve()) yield from self._validate(validator, instance) - # pylint: disable=unused-argument - def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): + def validate( + self, validator: Validator, _, instance: Any, schema: Any + ) -> ValidationResult: validator = validator.evolve( context=validator.context.evolve( functions=list(FUNCTIONS), strict_types=False, - ) + ), + function_filter=validator.function_filter.evolve( + add_cfn_lint_keyword=True, + ), ) t = instance.get("Type") @@ -95,7 +102,7 @@ def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): ).descend(path="Properties"), ) ) - for err in self.validate( + for err in self._validate_resource( region_validator, t, properties, schema.json_schema ): err.path.appendleft("Properties") @@ -116,7 +123,7 @@ def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): ) ) - for err in self.validate( + for err in self._validate_resource( region_validator, t, properties, cached_schema.json_schema ): err.path.appendleft("Properties") diff --git a/test/integration/test_schema_files.py b/test/integration/test_schema_files.py index aa68e394aa..3f283c41d8 100644 --- a/test/integration/test_schema_files.py +++ b/test/integration/test_schema_files.py @@ -33,11 +33,14 @@ class TestSchemaFiles(TestCase): "Metadata/AWS::CloudFormation::Interface", "Metadata/cfn-lint", "Outputs", + "Outputs/*/Condition", "Outputs/*/Export/Name", "Outputs/*/Value", "Parameters", "Parameters/*", + "Resources", "Resources/*", + "Resources/*/Condition", "Resources/*/DeletionPolicy", "Resources/*/DependsOn", "Resources/*/DependsOn/*", diff --git a/test/unit/module/rule/test_rule.py b/test/unit/module/rule/test_rule.py index 4e6341c93e..03746582b4 100644 --- a/test/unit/module/rule/test_rule.py +++ b/test/unit/module/rule/test_rule.py @@ -5,7 +5,7 @@ from test.testlib.testcase import BaseTestCase -from cfnlint.rules import CloudFormationLintRule # pylint: disable=E0401 +from cfnlint.rules import CloudFormationLintRule class TestCloudFormationRule(BaseTestCase): @@ -147,3 +147,17 @@ class TestRule(CloudFormationLintRule): rule.configure({}, True) self.assertFalse(rule.config.get("experimental")) + + def test_none_equal(self): + class TestRule(CloudFormationLintRule): + """Def Rule""" + + id = "E1000" + shortdesc = "Test Rule" + description = "Test Rule" + source_url = "https://github.com/aws-cloudformation/cfn-python-lint/" + tags = ["resources"] + + rule = TestRule() + + self.assertNotEqual(rule, None) diff --git a/test/unit/rules/conditions/test_condition.py b/test/unit/rules/conditions/test_condition.py new file mode 100644 index 0000000000..1c0950d731 --- /dev/null +++ b/test/unit/rules/conditions/test_condition.py @@ -0,0 +1,78 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.context import Path, create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.conditions.Condition import Condition +from cfnlint.template import Template + + +@pytest.fixture(scope="module") +def rule(): + rule = Condition() + yield rule + + +@pytest.fixture(scope="module") +def validator(): + cfn = Template( + "", + { + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]} + }, + }, + ) + context = create_context_for_template(cfn).evolve( + functions=[], + path=Path(path=deque(["Resources", "MyRdsDbInstance", "Properties"])), + ) + yield CfnTemplateValidator(schema={}, context=context, cfn=cfn) + + +@pytest.mark.parametrize( + "name,instance,errors", + [ + ("Valid with string", {"Condition": "IsUsEast1"}, []), + ( + "Invalid Type", + {"Condition": []}, + [ + ValidationError( + "[] is not of type 'string'", + validator="condition", + schema_path=deque(["type"]), + path=deque(["Condition"]), + ), + ValidationError( + "[] is not one of ['IsUsEast1']", + validator="condition", + schema_path=deque(["enum"]), + path=deque(["Condition"]), + ), + ], + ), + ( + "Non existent condition", + {"Condition": "IsUsWest2"}, + [ + ValidationError( + "'IsUsWest2' is not one of ['IsUsEast1']", + validator="condition", + schema_path=deque(["enum"]), + path=deque(["Condition"]), + ) + ], + ), + ], +) +def test_condition(name, instance, errors, rule, validator): + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == errors, name diff --git a/test/unit/rules/jsonschema/test_aws_type.py b/test/unit/rules/jsonschema/test_aws_type.py deleted file mode 100644 index 6d29ca1502..0000000000 --- a/test/unit/rules/jsonschema/test_aws_type.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 -""" - -from test.unit.rules import BaseRuleTestCase - -from cfnlint.jsonschema import CfnTemplateValidator, ValidationError -from cfnlint.rules import CloudFormationLintRule -from cfnlint.rules.jsonschema.AwsType import AwsType # pylint: disable=E0401 - - -class ValidType(CloudFormationLintRule): - id = "AnId" - - def foo(self, validator, awsType, instance, schema): - if instance == "bar": - return - yield ValidationError("Not bar") - - -class TestAwsType(BaseRuleTestCase): - """Test AWS Types""" - - def setUp(self): - """Setup""" - super(TestAwsType, self).setUp() - self.rule = AwsType() - self.rule.child_rules = {"AnId": ValidType()} - self.rule.types = { - "foo": "AnId", - "bar": "AnId", - } - - def test_aws_type(self): - validator = CfnTemplateValidator - self.assertListEqual( - list(self.rule.awsType(validator, "foo", "bar", {})), - [], - list(self.rule.awsType(validator, "foo", "bar", {})), - ) - self.assertListEqual( - list(self.rule.awsType(validator, "foo", "foo", {})), - [ValidationError("Not bar")], - ) - self.assertListEqual( - list(self.rule.awsType(validator, "foobar", {}, {})), - [], - ) - with self.assertRaises(ValueError): - list(self.rule.awsType(validator, "bar", {}, {})) diff --git a/test/unit/rules/outputs/test_condition.py b/test/unit/rules/outputs/test_condition.py new file mode 100644 index 0000000000..a5d8e652a1 --- /dev/null +++ b/test/unit/rules/outputs/test_condition.py @@ -0,0 +1,67 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.context import create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.outputs.Condition import Condition +from cfnlint.template import Template + + +@pytest.fixture(scope="module") +def rule(): + rule = Condition() + yield rule + + +@pytest.fixture(scope="module") +def cfn(): + return Template( + "", + { + "Conditions": { + "MyCondition": {"Fn::Equals": [{"Ref": "MyParam"}, "Foo"]}, + }, + }, + regions=["us-east-1"], + ) + + +@pytest.fixture(scope="module") +def context(cfn): + return create_context_for_template(cfn) + + +@pytest.mark.parametrize( + "name,instance,expected", + [ + ( + "Condition is valid", + "MyCondition", + [], + ), + ( + "Invalid condition", + "IsProduction", + [ + ValidationError( + "'IsProduction' is not one of ['MyCondition']", + validator="enum", + schema_path=deque(["enum"]), + rule=Condition(), + path=deque([]), + ) + ], + ), + ], +) +def test_validate(name, instance, expected, rule, context, cfn): + validator = CfnTemplateValidator(context=context, cfn=cfn) + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/resources/lmbd/test_snapstart.py b/test/unit/rules/resources/lmbd/test_snapstart.py index 66cee2e750..d7889375a4 100644 --- a/test/unit/rules/resources/lmbd/test_snapstart.py +++ b/test/unit/rules/resources/lmbd/test_snapstart.py @@ -7,7 +7,7 @@ import pytest -from cfnlint.context import Context +from cfnlint.context import Context, Path from cfnlint.jsonschema import CfnTemplateValidator, ValidationError from cfnlint.rules.resources.lmbd.SnapStart import SnapStart from cfnlint.template import Template @@ -96,7 +96,18 @@ def validator(): def test_validate(name, instance, path, expected, rule, validator): validator = validator.evolve( context=Context( - path=path, + path=Path( + path=path, + cfn_path=deque( + [ + "Resources", + "AWS::Lambda::Function", + "Properties", + "SnapStart", + "ApplyOn", + ] + ), + ) ) ) errs = list(rule.validate(validator, "", instance, {})) diff --git a/test/unit/rules/resources/test_condition.py b/test/unit/rules/resources/test_condition.py new file mode 100644 index 0000000000..ea8aeee7f3 --- /dev/null +++ b/test/unit/rules/resources/test_condition.py @@ -0,0 +1,67 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.context import create_context_for_template +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.resources.Condition import Condition +from cfnlint.template import Template + + +@pytest.fixture(scope="module") +def rule(): + rule = Condition() + yield rule + + +@pytest.fixture(scope="module") +def cfn(): + return Template( + "", + { + "Conditions": { + "MyCondition": {"Fn::Equals": [{"Ref": "MyParam"}, "Foo"]}, + }, + }, + regions=["us-east-1"], + ) + + +@pytest.fixture(scope="module") +def context(cfn): + return create_context_for_template(cfn) + + +@pytest.mark.parametrize( + "name,instance,expected", + [ + ( + "Condition is valid", + "MyCondition", + [], + ), + ( + "Invalid condition", + "IsProduction", + [ + ValidationError( + "'IsProduction' is not one of ['MyCondition']", + validator="enum", + schema_path=deque(["enum"]), + rule=Condition(), + path=deque([]), + ) + ], + ), + ], +) +def test_validate(name, instance, expected, rule, context, cfn): + validator = CfnTemplateValidator(context=context, cfn=cfn) + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/resources/test_configurations.py b/test/unit/rules/resources/test_configurations.py index 90001ca50f..e207f21d9e 100644 --- a/test/unit/rules/resources/test_configurations.py +++ b/test/unit/rules/resources/test_configurations.py @@ -21,9 +21,7 @@ def setUp(self): def test_configurations(self): validator = CfnTemplateValidator({}) errors = list( - self.rule.cfnresources( - validator, "cfnResources", {"foo": {"Type": "bar"}}, {} - ) + self.rule.validate(validator, "cfnResources", {"foo": {"Type": "bar"}}, {}) ) self.assertListEqual( errors, @@ -31,30 +29,16 @@ def test_configurations(self): errors, ) - errors = list( - self.rule.cfnresources(validator, "cfnResources", {"foo": []}, {}) - ) + errors = list(self.rule.validate(validator, "cfnResources", {"foo": []}, {})) self.assertListEqual( errors, [ - ValidationError( - ( - "[] should not be valid under {'required': " - "['CreationPolicy', 'UpdatePolicy']}" - ), - rule=Configuration(), - path=deque(["foo"]), - schema_path=deque(["additionalProperties", "then", "not"]), - validator="not", - validator_value={"required": ["CreationPolicy", "UpdatePolicy"]}, - instance=[], - ), ValidationError( "[] is not of type 'object'", rule=Configuration(), path=deque(["foo"]), - schema_path=deque(["additionalProperties", "type"]), + schema_path=deque(["patternProperties", "^[a-zA-Z0-9]+$", "type"]), validator="type", validator_value="object", instance=[], @@ -64,7 +48,7 @@ def test_configurations(self): ) errors = list( - self.rule.cfnresources(validator, "cfnResources", {"foo": {"Type": []}}, {}) + self.rule.validate(validator, "cfnResources", {"foo": {"Type": []}}, {}) ) self.assertListEqual( @@ -75,7 +59,13 @@ def test_configurations(self): rule=Configuration(), path=deque(["foo", "Type"]), schema_path=deque( - ["additionalProperties", "properties", "Type", "type"] + [ + "patternProperties", + "^[a-zA-Z0-9]+$", + "properties", + "Type", + "type", + ] ), validator="type", validator_value="string",