diff --git a/README.md b/README.md index 1e98b531..3df598ff 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ cfn test --enforce-timeout 60 #set the RL handler timeout to 60 seconds and CUD cfn test --enforce-timeout 60 -- -k contract_delete_update # combine args ``` +### Command: validate + +To validate the schema, use the `validate` command. + +This command is automatically run whenever one attempts to submit a resource or module. Any module fragments will be automatically validated via [`cfn-lint`](https://github.com/aws-cloudformation/cfn-python-lint/), however any warnings or errors detected by [`cfn-lint`](https://github.com/aws-cloudformation/cfn-python-lint/) will not cause this step to fail. + +```bash +cfn validate +``` + ### Command: build-image To build an image for a resource type. This image provides a minimalistic execution environment for the resource handler that does not depend on AWS Lambda in anyway. This image can be used during cfn invoke and cfn test instead of using sam cli. diff --git a/setup.cfg b/setup.cfg index c3d76d28..f157bce0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ include_trailing_comma = true combine_as_imports = True force_grid_wrap = 0 known_first_party = rpdk -known_third_party = boto3,botocore,colorama,docker,hypothesis,jinja2,jsonschema,ordered_set,pkg_resources,pytest,pytest_localserver,setuptools,yaml +known_third_party = boto3,botocore,cfnlint,colorama,docker,hypothesis,jinja2,jsonschema,ordered_set,pkg_resources,pytest,pytest_localserver,setuptools,yaml [tool:pytest] # can't do anything about 3rd part modules, so don't spam us diff --git a/setup.py b/setup.py index 0553b776..14f94c51 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ def find_version(*file_paths): "colorama>=0.4.1", "docker>=4.3.1", "ordered-set>=4.0.2", + "cfn-lint>=0.43.0", ], entry_points={ "console_scripts": ["cfn-cli = rpdk.core.cli:main", "cfn = rpdk.core.cli:main"] diff --git a/src/rpdk/core/fragment/generator.py b/src/rpdk/core/fragment/generator.py index 775d59ed..161e549a 100644 --- a/src/rpdk/core/fragment/generator.py +++ b/src/rpdk/core/fragment/generator.py @@ -11,6 +11,8 @@ import os from pathlib import Path +import cfnlint.config +import cfnlint.core import yaml from rpdk.core.data_loaders import resource_json @@ -75,11 +77,51 @@ def validate_fragments(self): self.__validate_no_transforms_present(raw_fragments) self.__validate_outputs(raw_fragments) self.__validate_mappings(raw_fragments) + self.__validate_fragment_thru_cfn_lint(raw_fragments) + + def __validate_fragment_thru_cfn_lint(self, raw_fragments): + lint_warnings = self.__get_cfn_lint_matches(raw_fragments) + if not lint_warnings: + LOG.warning("Module fragment is valid.") + else: + LOG.warning( + "Module fragment is probably valid, but there are " + "warnings/errors from cfn-lint " + "(https://github.com/aws-cloudformation/cfn-python-lint):" + ) + for lint_warning in lint_warnings: + print( + "\t{} (from rule {})".format( + lint_warning.message, lint_warning.rule + ), + ) def __validate_outputs(self, raw_fragments): self.__validate_no_exports_present(raw_fragments) self.__validate_output_limit(raw_fragments) + @staticmethod + def __get_cfn_lint_matches(raw_fragment): + filename = "temporary_fragment.json" + + with open(filename, "w") as outfile: + json.dump(raw_fragment, outfile, indent=4) + + template = cfnlint.decode.cfn_json.load(filename) + + # Initialize the ruleset to be applied (no overrules, no excludes) + rules = cfnlint.core.get_rules([], [], [], [], False, []) + + # Default region used by cfn-lint + regions = ["us-east-1"] + + # Runs Warning and Error rules + matches = cfnlint.core.run_checks(filename, template, rules, regions) + + os.remove(filename) + + return matches + @staticmethod def __validate_no_exports_present(raw_fragments): if "Outputs" in raw_fragments: diff --git a/src/rpdk/core/project.py b/src/rpdk/core/project.py index 63a27fc2..efdc7460 100644 --- a/src/rpdk/core/project.py +++ b/src/rpdk/core/project.py @@ -428,6 +428,7 @@ def load(self): LOG.info("Validating your resource specification...") try: self.load_schema() + LOG.warning("Resource schema is valid.") except FileNotFoundError as e: self._raise_invalid_project("Resource specification not found.", e) except SpecValidationError as e: diff --git a/src/rpdk/core/validate.py b/src/rpdk/core/validate.py index be2ad40b..d5380bfc 100644 --- a/src/rpdk/core/validate.py +++ b/src/rpdk/core/validate.py @@ -13,8 +13,6 @@ def validate(_args): project = Project() project.load() - LOG.warning("Resource schema for %s is valid", project.type_name) - def setup_subparser(subparsers, parents): parser = subparsers.add_parser("validate", description=__doc__, parents=parents) diff --git a/tests/data/sample_fragments/template_without_parameter_section.json b/tests/data/sample_fragments/template_without_parameter_section.json index 6b5278d8..aa479e97 100644 --- a/tests/data/sample_fragments/template_without_parameter_section.json +++ b/tests/data/sample_fragments/template_without_parameter_section.json @@ -5,6 +5,7 @@ "S3Bucket": { "Type": "AWS::S3::Bucket", "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", "Properties": { "VersioningConfiguration": { "Status": "Enabled" diff --git a/tests/test_project.py b/tests/test_project.py index 71ec0f9e..9fbeb89e 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -653,6 +653,17 @@ def test_load_module_project_succeeds(project): project.load() +def test_load_resource_succeeds(project): + project.artifact_type = "Resource" + project.type_name = "Unit::Test::Resource" + patch_load_settings = patch.object( + project, "load_settings", return_value={"artifact_type": "RESOURCE"} + ) + project._write_example_schema() + with patch_load_settings: + project.load() + + def test_load_module_project_with_invalid_fragments(project): project.artifact_type = "MODULE" project.type_name = "Unit::Test::Malik::MODULE"