Skip to content

Commit 839feec

Browse files
committed
Expand deployment file capabilities
1 parent c56d4f9 commit 839feec

37 files changed

+593
-381
lines changed

src/cfnlint/config.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import logging
1313
import os
1414
import sys
15+
from copy import deepcopy
1516
from pathlib import Path
1617
from typing import Any, Dict, Sequence, TypedDict
1718

1819
from typing_extensions import Unpack
1920

2021
import cfnlint.decode.cfn_yaml
22+
from cfnlint.context.parameters import ParameterSet
2123
from cfnlint.helpers import REGIONS, format_json_string
2224
from cfnlint.jsonschema import StandardValidator
2325
from cfnlint.version import __version__
@@ -681,15 +683,15 @@ class ManualArgs(TypedDict, total=False):
681683
non_zero_exit_code: str
682684
output_file: str
683685
regions: list
684-
parameters: list[dict[str, Any]]
686+
parameters: list[ParameterSet]
687+
templates: list[str]
685688

686689

687690
# pylint: disable=too-many-public-methods
688691
class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs):
689692

690693
def __init__(self, cli_args: list[str] | None = None, **kwargs: Unpack[ManualArgs]):
691694
self._manual_args = kwargs or ManualArgs()
692-
self._templates_to_process = False
693695
CliArgs.__init__(self, cli_args)
694696
# configure debug as soon as we can
695697
TemplateArgs.__init__(self, {})
@@ -716,12 +718,17 @@ def __repr__(self):
716718
"merge_configs": self.merge_configs,
717719
"non_zero_exit_code": self.non_zero_exit_code,
718720
"override_spec": self.override_spec,
719-
"regions": self.regions,
720721
"parameters": self.parameters,
722+
"regions": self.regions,
721723
"templates": self.templates,
722724
}
723725
)
724726

727+
def __eq__(self, value):
728+
if not isinstance(value, ConfigMixIn):
729+
return False
730+
return str(self) == str(value)
731+
725732
def _get_argument_value(self, arg_name, is_template, is_config_file):
726733
cli_value = getattr(self.cli_args, arg_name)
727734
template_value = self.template_args.get(arg_name)
@@ -816,7 +823,9 @@ def templates(self):
816823
file_args = self._get_argument_value("templates", False, True)
817824
cli_args = self._get_argument_value("templates", False, False)
818825

819-
if cli_alt_args:
826+
if "templates" in self._manual_args:
827+
filenames = self._manual_args["templates"]
828+
elif cli_alt_args:
820829
filenames = cli_alt_args
821830
elif file_args:
822831
filenames = file_args
@@ -826,10 +835,6 @@ def templates(self):
826835
# No filenames found, could be piped in or be using the api.
827836
return None
828837

829-
# If we're still haven't returned, we've got templates to lint.
830-
# Build up list of templates to lint.
831-
self.templates_to_process = True
832-
833838
if isinstance(filenames, str):
834839
filenames = [filenames]
835840

@@ -880,12 +885,21 @@ def append_rules(self):
880885
)
881886

882887
@property
883-
def parameters(self):
884-
return self._get_argument_value("parameters", True, True)
888+
def parameters(self) -> list[ParameterSet]:
889+
parameter_sets = self._get_argument_value("parameters", True, True)
890+
results: list[ParameterSet] = []
891+
for parameter_set in parameter_sets:
892+
if isinstance(parameter_set, ParameterSet):
893+
results.append(parameter_set)
894+
else:
895+
results.append(
896+
ParameterSet(
897+
source=None,
898+
parameters=parameter_set,
899+
)
900+
)
885901

886-
@parameters.setter
887-
def parameters(self, parameters: list[dict[str, Any]]):
888-
self._manual_args["parameters"] = parameters
902+
return results
889903

890904
@property
891905
def override_spec(self):
@@ -952,10 +966,8 @@ def non_zero_exit_code(self):
952966
def force(self):
953967
return self._get_argument_value("force", False, False)
954968

955-
@property
956-
def templates_to_process(self):
957-
return self._templates_to_process
969+
def evolve(self, **kwargs: Unpack[ManualArgs]) -> "ConfigMixIn":
958970

959-
@templates_to_process.setter
960-
def templates_to_process(self, value: bool):
961-
self._templates_to_process = value
971+
config = deepcopy(self)
972+
config._manual_args.update(kwargs)
973+
return config

src/cfnlint/context/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
__all__ = ["Context", "create_context_for_template"]
1+
__all__ = ["Context", "create_context_for_template", "ParameterSet"]
22

3-
from cfnlint.context.context import Context, Path, create_context_for_template
3+
from cfnlint.context.context import (
4+
Context,
5+
ParameterSet,
6+
Path,
7+
create_context_for_template,
8+
)

src/cfnlint/context/context.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from collections import deque
1010
from dataclasses import InitVar, dataclass, field, fields
1111
from functools import lru_cache
12-
from typing import Any, Deque, Iterator, Sequence, Set, Tuple
12+
from typing import TYPE_CHECKING, Any, Deque, Iterator, Set, Tuple
1313

1414
from cfnlint.context._mappings import Mappings
1515
from cfnlint.context.conditions._conditions import Conditions
16+
from cfnlint.context.parameters import ParameterSet
1617
from cfnlint.helpers import (
1718
BOOLEAN_STRINGS_TRUE,
1819
FUNCTIONS,
@@ -22,6 +23,9 @@
2223
)
2324
from cfnlint.schema import PROVIDER_SCHEMA_MANAGER, AttributeDict
2425

26+
if TYPE_CHECKING:
27+
from cfnlint.template import Template
28+
2529
_PSEUDOPARAMS_NON_REGION = ["AWS::AccountId", "AWS::NoValue", "AWS::StackName"]
2630

2731

@@ -132,12 +136,12 @@ class Context:
132136
"""
133137

134138
# what regions we are processing
135-
regions: Sequence[str] = field(
139+
regions: list[str] = field(
136140
init=True, default_factory=lambda: list([REGION_PRIMARY])
137141
)
138142

139143
# supported functions at this point in the template
140-
functions: Sequence[str] = field(init=True, default_factory=list)
144+
functions: list[str] = field(init=True, default_factory=list)
141145

142146
path: Path = field(init=True, default_factory=Path)
143147

@@ -153,7 +157,7 @@ class Context:
153157
init=True, default_factory=lambda: set(PSEUDOPARAMS)
154158
)
155159

156-
# Combiniation of storing any resolved ref
160+
# Combination of storing any resolved ref
157161
# and adds in any Refs available from things like Fn::Sub
158162
ref_values: dict[str, Any] = field(init=True, default_factory=dict)
159163

@@ -163,6 +167,9 @@ class Context:
163167
is_resolved_value: bool = field(init=True, default=False)
164168
resolve_pseudo_parameters: bool = field(init=True, default=True)
165169

170+
# Deployment parameters
171+
parameter_sets: list[ParameterSet] | None = field(init=True, default_factory=list)
172+
166173
def evolve(self, **kwargs) -> "Context":
167174
"""
168175
Create a new context without merging together attributes
@@ -441,7 +448,9 @@ def _init_transforms(transforms: Any) -> Transforms:
441448
return Transforms([])
442449

443450

444-
def create_context_for_template(cfn):
451+
def create_context_for_template(
452+
cfn: Template,
453+
) -> "Context":
445454
parameters = {}
446455
try:
447456
parameters = _init_parameters(cfn.template.get("Parameters", {}))
@@ -476,5 +485,6 @@ def create_context_for_template(cfn):
476485
regions=cfn.regions,
477486
path=Path(),
478487
functions=["Fn::Transform"],
479-
ref_values=cfn.parameters or {},
488+
ref_values={},
489+
parameter_sets=[],
480490
)

src/cfnlint/context/parameters.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from dataclasses import dataclass, field
9+
from typing import Any
10+
11+
12+
@dataclass
13+
class ParameterSet:
14+
15+
source: str | None = field(default=None)
16+
parameters: dict[str, Any] = field(default_factory=dict)

src/cfnlint/core.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@
1212
from cfnlint.exceptions import UnexpectedRuleException
1313
from cfnlint.match import Match
1414
from cfnlint.rules import RulesCollection
15-
16-
from cfnlint.runner.exceptions import UnexpectedRuleException
1715
from cfnlint.runner.template.runner import _run_template
1816

1917

20-
2118
def get_rules(
2219
append_rules: list[str],
2320
ignore_rules: list[str],

src/cfnlint/decode/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
SPDX-License-Identifier: MIT-0
44
"""
55

6+
__all__ = [
7+
"create_match_file_error",
8+
"create_match_json_parser_error",
9+
"create_match_yaml_parser_error",
10+
"decode",
11+
"decode_str",
12+
"convert_dict",
13+
]
14+
615
from cfnlint.decode.decode import (
716
create_match_file_error,
817
create_match_json_parser_error,

src/cfnlint/decode/decode.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _decode(
3737
) -> Decode:
3838
"""Decode payload using yaml_f and json_f, using filename for log output."""
3939
template = None
40-
matches = []
40+
matches: Matches = []
4141
try:
4242
template = yaml_f(payload)
4343
except IOError as e:
@@ -145,7 +145,10 @@ def _decode(
145145
message="Template needs to be an object.",
146146
)
147147
]
148-
return (template, matches)
148+
149+
if matches:
150+
return None, matches
151+
return template, matches
149152

150153

151154
def create_match_yaml_parser_error(parser_error, filename):

src/cfnlint/exceptions.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""
2+
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
19
class CfnLintExitException(Exception):
210
"""
311
An exception that is raised to indicate that the CloudFormation linter should exit.
@@ -10,11 +18,11 @@ class CfnLintExitException(Exception):
1018
exit_code (int): The exit code to be used when the linter exits.
1119

1220
Methods:
13-
__init__(self, exit_code: int) -> None:
21+
__init__(self, msg: str | None=None, exit_code: int=1) -> None:
1422
Initialize a new CfnLintExitException instance with the specified exit code.
1523
"""
1624

17-
def __init__(self, msg=None, exit_code=1):
25+
def __init__(self, msg: str | None = None, exit_code: int = 1):
1826
"""
1927
Initialize a new CfnLintExitException instance with the specified exit code.
2028

@@ -49,9 +57,17 @@ class UnexpectedRuleException(CfnLintExitException):
4957

5058
class DuplicateRuleError(CfnLintExitException):
5159
"""
52-
The data associated with a particular path could not be loaded.
53-
:ivar data_path: The data path that the user attempted to load.
60+
An exception that is raised when an unexpected error occurs while loading rules.
61+
62+
This exception is raised when the CloudFormation linter encounters a rule with a
63+
duplicate ID.
5464
"""
5565

5666
def __init__(self, rule_id: str):
67+
"""
68+
Initialize a new CfnLintExitException instance with the specified exit code.
69+
70+
Args:
71+
rule_id (str): The rule ID that a duplicate was found for.
72+
"""
5773
super().__init__(f"Rule already included: {rule_id}")

src/cfnlint/helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import dataclasses
1011
import datetime
1112
import fnmatch
1213
import gzip
@@ -627,6 +628,8 @@ def converter(o): # pylint: disable=R1710
627628
"""Help convert date/time into strings"""
628629
if isinstance(o, datetime.datetime):
629630
return o.__str__() # pylint: disable=unnecessary-dunder-call
631+
elif dataclasses.is_dataclass(o):
632+
return dataclasses.asdict(o)
630633

631634
return json.dumps(
632635
json_string, indent=1, sort_keys=True, separators=(",", ": "), default=converter

src/cfnlint/jsonschema/validators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from typing import Any, Callable
2626

2727
from cfnlint.conditions import UnknownSatisfisfaction
28-
from cfnlint.context import Context, create_context_for_template
28+
from cfnlint.context import Context
2929
from cfnlint.helpers import is_function
3030
from cfnlint.jsonschema import _keywords, _keywords_cfn, _resolvers_cfn
3131
from cfnlint.jsonschema._filter import FunctionFilter
@@ -92,7 +92,7 @@ class Validator:
9292

9393
def __post_init__(self):
9494
if self.context is None:
95-
self.context = create_context_for_template(self.cfn)
95+
self.context = self.cfn.context.evolve()
9696
if self.resolver is None:
9797
self.resolver = RefResolver.from_schema(
9898
schema=self.schema,

src/cfnlint/match.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ def create(
118118
if linenumberend is None:
119119
linenumberend = linenumber
120120

121+
filename = getattr(rulematch_obj, "filename", filename)
122+
121123
return cls(
122124
linenumber=linenumber,
123125
columnnumber=columnnumber,

src/cfnlint/rules/deployment_files/Parameters.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,23 @@ def _build_schema(self, instance: Any) -> dict[str, Any]:
7979
return schema
8080

8181
def validate(self, validator: Validator, _: Any, instance: Any, schema: Any):
82-
if validator.cfn.parameters is None:
82+
if validator.context.parameter_sets is None:
8383
return
8484

85-
cfn_validator = self.extend_validator(
86-
validator=validator,
87-
schema=self._build_schema(instance),
88-
context=validator.context,
89-
).evolve(
90-
context=validator.context.evolve(strict_types=False),
91-
function_filter=validator.function_filter.evolve(
92-
add_cfn_lint_keyword=False,
93-
),
94-
)
95-
96-
yield from super()._iter_errors(cfn_validator, validator.cfn.parameters)
85+
for parameter_set in validator.context.parameter_sets:
86+
87+
cfn_validator = self.extend_validator(
88+
validator=validator,
89+
schema=self._build_schema(instance),
90+
context=validator.context,
91+
).evolve(
92+
context=validator.context.evolve(strict_types=False),
93+
function_filter=validator.function_filter.evolve(
94+
add_cfn_lint_keyword=False,
95+
),
96+
)
97+
98+
for err in super()._iter_errors(cfn_validator, parameter_set.parameters):
99+
if parameter_set.source:
100+
err.extra_args["filename"] = parameter_set.source
101+
yield err

0 commit comments

Comments
 (0)