Skip to content

Commit

Permalink
Allow for deployment files
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong committed Dec 18, 2024
1 parent 5c5d1a4 commit 6ba216e
Show file tree
Hide file tree
Showing 27 changed files with 521 additions and 381 deletions.
14 changes: 10 additions & 4 deletions src/cfnlint/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from cfnlint.decode.decode import decode_str
from cfnlint.helpers import REGION_PRIMARY, REGIONS
from cfnlint.rules import Match, RulesCollection
from cfnlint.runner import Runner, TemplateRunner
from cfnlint.runner import Runner
from cfnlint.runner.template import run_template_by_data

Matches = List[Match]

Expand Down Expand Up @@ -56,11 +57,16 @@ def lint(
config_mixin = ConfigMixIn(**config)

if isinstance(rules, RulesCollection):
template_runner = TemplateRunner(None, template, config_mixin, rules) # type: ignore # noqa: E501
return list(template_runner.run())
return list(
run_template_by_data(
template,
config_mixin,
rules, # type: ignore
)
)

runner = Runner(config_mixin)
return list(runner.validate_template(None, template))
return list(runner.validate_template(template))


def lint_all(s: str) -> list[Match]:
Expand Down
31 changes: 31 additions & 0 deletions src/cfnlint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ def comma_separated_arg(string):
return string.split(",")


class key_value(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, dict())

for value in values:
# split it into key and value
key, value = value.split("=", 1)
# assign into dictionary
getattr(namespace, self.dest)[key.strip()] = value.strip()


def _ensure_value(namespace, name, value):
if getattr(namespace, name, None) is None:
setattr(namespace, name, value)
Expand Down Expand Up @@ -400,6 +411,16 @@ def __call__(self, parser, namespace, values, option_string=None):
default=[],
action="extend",
)
standard.add_argument(
"-tp",
"--template-parameters",
dest="template_parameters",
metavar="KEY=VALUE",
nargs="+",
default={},
action=key_value,
help="only check rules whose id do not match these values",
)
advanced.add_argument(
"-D", "--debug", help="Enable debug logging", action="store_true"
)
Expand Down Expand Up @@ -616,6 +637,7 @@ class ManualArgs(TypedDict, total=False):
configure_rules: dict[str, dict[str, Any]]
include_checks: list[str]
ignore_checks: list[str]
template_parameters: dict[str, Any]
mandatory_checks: list[str]
include_experimental: bool
ignore_bad_template: bool
Expand Down Expand Up @@ -646,6 +668,7 @@ def __repr__(self):
"ignore_checks": self.ignore_checks,
"include_checks": self.include_checks,
"mandatory_checks": self.mandatory_checks,
"template_parameters": self.template_parameters,
"include_experimental": self.include_experimental,
"configure_rules": self.configure_rules,
"regions": self.regions,
Expand Down Expand Up @@ -817,6 +840,14 @@ def append_rules(self):
"append_rules", False, True
)

@property
def template_parameters(self):
return self._get_argument_value("template_parameters", True, True)

@template_parameters.setter
def template_parameters(self, template_parameters: dict[str, Any]):
self._manual_args["template_parameters"] = template_parameters

@property
def override_spec(self):
return self._get_argument_value("override_spec", False, True)
Expand Down
1 change: 1 addition & 0 deletions src/cfnlint/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,5 @@ def create_context_for_template(cfn):
regions=cfn.regions,
path=Path(),
functions=["Fn::Transform"],
ref_values=cfn.parameters,
)
17 changes: 9 additions & 8 deletions src/cfnlint/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from cfnlint.config import _DEFAULT_RULESDIR, ConfigMixIn, ManualArgs
from cfnlint.match import Match
from cfnlint.rules import RulesCollection
from cfnlint.runner import TemplateRunner, UnexpectedRuleException
from cfnlint.runner.exceptions import UnexpectedRuleException
from cfnlint.runner.template.runner import _run_template


def get_rules(
Expand Down Expand Up @@ -68,11 +69,11 @@ def run_checks(
**config,
)

runner = TemplateRunner(
filename=filename,
template=template,
rules=rules, # type: ignore
config=config_mixin,
return list(
_run_template(
filename=filename,
template=template,
rules=rules, # type: ignore
config=config_mixin,
)
)

return list(runner.run())
106 changes: 106 additions & 0 deletions src/cfnlint/rules/parameters/DeploymentParameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from __future__ import annotations

from typing import Any

from cfnlint.jsonschema import Validator
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema


class DeploymentParameters(CfnLintJsonSchema):
"""Check if Parameters are configured correctly"""

id = "E2900"
shortdesc = "Parameters have appropriate properties"
description = "Making sure the parameters are properly configured"
source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html"
tags = ["parameters"]

def __init__(self):
"""Init"""
super().__init__(
keywords=["Parameters"],
all_matches=True,
)

def _is_type_a_list(self, parameter_type: str) -> bool:
return "List" in parameter_type and "CommaDelimitedList" not in parameter_type

def _build_schema(self, instance: Any) -> dict[str, Any]:
if not isinstance(instance, dict):
return {}

schema: dict[str, Any] = {
"properties": {},
"additionalProperties": False,
"required": [],
"type": "object",
}

singular_types = ["string", "integer", "number", "boolean"]

for parameter_name, parameter_object in instance.items():
schema["properties"][parameter_name] = {}
if not isinstance(parameter_object, dict):
continue
if "Default" not in parameter_object:
schema["required"] = [parameter_name]

parameter_type = parameter_object.get("Type")
if not isinstance(parameter_type, str):
continue

if self._is_type_a_list(parameter_type):
schema["properties"][parameter_name] = {
"type": "array",
"items": {
"type": singular_types,
},
}
if "AllowedValues" in parameter_object:
schema["properties"][parameter_name]["items"]["enum"] = (
parameter_object["AllowedValues"]
)
if "Pattern" in parameter_object:
if self._is_type_a_list(parameter_type):
schema["properties"][parameter_name]["items"]["pattern"] = (
parameter_object["Pattern"]
)
else:
schema["properties"][parameter_name]["type"] = singular_types
if "AllowedValues" in parameter_object:
schema["properties"][parameter_name]["enum"] = parameter_object[
"AllowedValues"
]
if "Pattern" in parameter_object:
schema["properties"][parameter_name]["pattern"] = parameter_object[
"Pattern"
]

return schema

def validate(self, validator: Validator, _: Any, instance: Any, schema: Any):
if not validator.cfn.parameters:
return

cfn_validator = self.extend_validator(
validator=validator,
schema=self._build_schema(instance),
context=validator.context,
).evolve(
context=validator.context.evolve(strict_types=False),
function_filter=validator.function_filter.evolve(
add_cfn_lint_keyword=False,
),
)

for err in super()._iter_errors(cfn_validator, validator.cfn.parameters):
# we use enum twice. Once for the type and once for the property
# names. There are separate error numbers so we do this.
if "propertyNames" in err.schema_path and "enum" in err.schema_path:
err.rule = self
yield err
3 changes: 1 addition & 2 deletions src/cfnlint/runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
SPDX-License-Identifier: MIT-0
"""

__all__ = ["main", "Runner", "TemplateRunner"]
__all__ = ["main", "Runner"]

from cfnlint.runner.cli import Runner, main
from cfnlint.runner.template import TemplateRunner
39 changes: 13 additions & 26 deletions src/cfnlint/runner/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@
import cfnlint.formatters
import cfnlint.maintenance
from cfnlint.config import ConfigMixIn, configure_logging
from cfnlint.decode.decode import decode
from cfnlint.rules import Match, Rules
from cfnlint.rules.errors import ConfigError, ParseError
from cfnlint.runner.deployment_file.runner import run_deployment_files
from cfnlint.runner.exceptions import CfnLintExitException, UnexpectedRuleException
from cfnlint.runner.template import TemplateRunner
from cfnlint.runner.template import run_template_by_data, run_template_by_file_path
from cfnlint.schema import PROVIDER_SCHEMA_MANAGER
from cfnlint.runner.deployment_files.runner import run_deployment_files

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -119,7 +118,6 @@ def _get_rules(self) -> None:
self.rules.update(Rules.create_from_directory(rules_path))
else:
self.rules.update(Rules.create_from_module(rules_path))

self.rules.update(
Rules.create_from_custom_rules_file(self.config.custom_rules)
)
Expand Down Expand Up @@ -159,39 +157,28 @@ def _validate_filenames(self, filenames: Sequence[str | None]) -> Iterator[Match
):
ignore_bad_template = True
for filename in filenames:
(template, matches) = decode(filename)
if matches:
if ignore_bad_template or any(
"E0000".startswith(x) for x in self.config.ignore_checks
):
matches = [match for match in matches if match.rule.id != "E0000"]

yield from iter(matches)
continue
yield from self.validate_template(filename, template) # type: ignore[arg-type] # noqa: E501

def validate_template(
self, filename: str | None, template: dict[str, Any]
) -> Iterator[Match]:
yield from run_template_by_file_path(
filename, self.config, self.rules, ignore_bad_template
)

def validate_template(self, template: dict[str, Any]) -> Iterator[Match]:
"""
Validate a single CloudFormation template and yield any matches found.
This function takes a CloudFormation template as a dictionary and runs the
configured rules against it. Any matches found are yielded as an iterator.
This function decodes the provided template, validates it against the
configured rules, and yields any matches found as an iterator.
Args:
filename (str | None): The filename of the CloudFormation template, or
`None` if the template is not associated with a file.
template (dict[str, Any]): The CloudFormation template as a dictionary.
filename (str | None): The filename of the template being validated.
template (dict[str, Any]): The CloudFormation template to be validated.
Yields:
Match: The matches found during the validation process.
Raises:
None: This function does not raise any exceptions.
"""
runner = TemplateRunner(filename, template, self.config, self.rules)
yield from runner.run()
yield from run_template_by_data(template, self.config, self.rules)

def _cli_output(self, matches: list[Match]) -> None:
formatter = get_formatter(self.config)
Expand Down Expand Up @@ -269,7 +256,7 @@ def run(self) -> Iterator[Match]:
yield from self._validate_filenames(self.config.templates)
return

yield from run_deployment_files(self.config)
yield from run_deployment_files(self.config, self.rules)

def cli(self) -> None:
"""
Expand Down
15 changes: 15 additions & 0 deletions src/cfnlint/runner/deployment_file/deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from dataclasses import dataclass, field
from typing import Any


@dataclass(frozen=True)
class Deployment:

template_file_path: str = field()
parameters: dict[str, Any] = field(default_factory=dict)
tags: dict[str, str] = field(default_factory=dict)
10 changes: 10 additions & 0 deletions src/cfnlint/runner/deployment_file/deployment_types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

__all__ = ["create_deployment_from_git_sync"]

from cfnlint.runner.deployment_file.deployment_types.git_sync import (
create_deployment_from_git_sync,
)
20 changes: 20 additions & 0 deletions src/cfnlint/runner/deployment_file/deployment_types/git_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from typing import Any

from cfnlint.runner.deployment_file.deployment import Deployment


def create_deployment_from_git_sync(data: dict[str, Any]) -> Deployment:

template_file_path = data.get("template-file-path")
if not template_file_path:
raise ValueError("template-file-path is required")
parameters = data.get("parameters", {})
tags = data.get("tags", {})
return Deployment(
template_file_path=template_file_path, parameters=parameters, tags=tags
)
Loading

0 comments on commit 6ba216e

Please sign in to comment.