Skip to content

Commit

Permalink
Fail gracefully if no templates match wildcard (#3603)
Browse files Browse the repository at this point in the history
*Description of changes:*

`cfn-lint` currently fails if there are no Cloudformation templates that
match the pattern specified either via CLI arguments or via config file.

```
$ cfn-lint "cfn/**/*.y*ml"
2024-08-16 13:41:38,381 - cfnlint.decode.decode - ERROR - Template file not found: cfn/**/*.y*ml
E0000 Template file not found: cfn/**/*.y*ml
cfn/**/*.y*ml:1:1

$ echo $?
2
```

It appears that when the glob pattern matching does not find any match,
the actual string is appended as a template file.

This PR improves the handling of wildcard templates by ensuring only
matched templates are added to be linted by `cfn-lint` and would
gracefully exit without an output message.

If run with debug switch, it lists the Cloudformation templates found by the glob.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Co-authored-by: Kevin DeJong <[email protected]>
  • Loading branch information
thecodingsysadmin and kddejong authored Sep 9, 2024
1 parent 67e39ee commit e2a6c83
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 38 deletions.
60 changes: 46 additions & 14 deletions src/cfnlint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs):

def __init__(self, cli_args: list[str] | None = None, **kwargs: Unpack[ManualArgs]):
self._manual_args = kwargs or ManualArgs()
self._templates_to_process = False
CliArgs.__init__(self, cli_args)
# configure debug as soon as we can
TemplateArgs.__init__(self, {})
Expand Down Expand Up @@ -732,23 +733,47 @@ def format(self):

@property
def templates(self):
templates_args = self._get_argument_value("templates", False, True)
template_alt_args = self._get_argument_value("template_alt", False, False)
if template_alt_args:
filenames = template_alt_args
elif templates_args:
filenames = templates_args
"""
Returns a list of Cloudformation templates to lint.
Order of precedence:
- Filenames provided via `-t` CLI
- Filenames specified in the config file.
- Arguments provided via `cfn-lint` CLI.
"""

all_filenames = []

cli_alt_args = self._get_argument_value("template_alt", False, False)
file_args = self._get_argument_value("templates", False, True)
cli_args = self._get_argument_value("templates", False, False)

if cli_alt_args:
filenames = cli_alt_args
elif file_args:
filenames = file_args
elif cli_args:
filenames = cli_args
else:
# No filenames found, could be piped in or be using the api.
return None

# if only one is specified convert it to array
# If we're still haven't returned, we've got templates to lint.
# Build up list of templates to lint.
self.templates_to_process = True

if isinstance(filenames, str):
filenames = [filenames]

ignore_templates = self._ignore_templates()
all_filenames = self._glob_filenames(filenames)
all_filenames.extend(self._glob_filenames(filenames))

return [i for i in all_filenames if i not in ignore_templates]
found_files = [i for i in all_filenames if i not in ignore_templates]
LOGGER.debug(
f"List of Cloudformation Templates to lint: {found_files} from {filenames}"
)
return found_files

def _ignore_templates(self):
ignore_template_args = self._get_argument_value("ignore_templates", False, True)
Expand All @@ -770,12 +795,11 @@ def _glob_filenames(self, filenames: Sequence[str]) -> list[str]:

for filename in filenames:
add_filenames = glob.glob(filename, recursive=True)
# only way to know of the glob failed is to test it
# then add the filename as requested
if not add_filenames:
all_filenames.append(filename)
else:

if isinstance(add_filenames, list):
all_filenames.extend(add_filenames)
else:
LOGGER.error(f"{filename} could not be processed by glob.glob")

return sorted(list(map(str, map(Path, all_filenames))))

Expand Down Expand Up @@ -845,3 +869,11 @@ def non_zero_exit_code(self):
@property
def force(self):
return self._get_argument_value("force", False, False)

@property
def templates_to_process(self):
return self._templates_to_process

@templates_to_process.setter
def templates_to_process(self, value: bool):
self._templates_to_process = value
16 changes: 7 additions & 9 deletions src/cfnlint/decode/cfn_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,13 @@ def load(filename):

content = ""

if not sys.stdin.isatty():
filename = "-" if filename is None else filename
if sys.version_info.major <= 3 and sys.version_info.minor <= 9:
for line in fileinput.input(files=filename):
content = content + line
else:
for line in fileinput.input( # pylint: disable=unexpected-keyword-arg
files=filename, encoding="utf-8"
):
if (filename is None) and (not sys.stdin.isatty()):
filename = "-" # no filename provided, it's stdin
fileinput_args = {"files": filename}
if sys.version_info.major <= 3 and sys.version_info.minor >= 10:
fileinput_args["encoding"] = "utf-8"
with fileinput.input(**fileinput_args) as f:
for line in f:
content = content + line
else:
with open(filename, encoding="utf-8") as fp:
Expand Down
16 changes: 7 additions & 9 deletions src/cfnlint/decode/cfn_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,15 +304,13 @@ def load(filename):

content = ""

if not sys.stdin.isatty():
filename = "-" if filename is None else filename
if sys.version_info.major <= 3 and sys.version_info.minor <= 9:
for line in fileinput.input(files=filename):
content = content + line
else:
for line in fileinput.input( # pylint: disable=unexpected-keyword-arg
files=filename, encoding="utf-8"
):
if (filename is None) and (not sys.stdin.isatty()):
filename = "-" # no filename provided, it's stdin
fileinput_args = {"files": filename}
if sys.version_info.major <= 3 and sys.version_info.minor >= 10:
fileinput_args["encoding"] = "utf-8"
with fileinput.input(**fileinput_args) as f:
for line in f:
content = content + line
else:
with open(filename, encoding="utf-8") as fp:
Expand Down
6 changes: 4 additions & 2 deletions src/cfnlint/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def __init__(self, config: ConfigMixIn) -> None:
settings for the template scan.
"""
self.config = config
self.config.templates
self.formatter = get_formatter(self.config)
self.rules: Rules = Rules()
self._get_rules()
Expand Down Expand Up @@ -388,7 +389,8 @@ def run(self) -> Iterator[Match]:
Raises:
None: This function does not raise any exceptions.
"""
if not sys.stdin.isatty() and not self.config.templates:

if (not sys.stdin.isatty()) and (not self.config.templates_to_process):
yield from self._validate_filenames([None])
return

Expand Down Expand Up @@ -434,7 +436,7 @@ def cli(self) -> None:
print(self.rules)
sys.exit(0)

if not self.config.templates:
if not self.config.templates_to_process:
if sys.stdin.isatty():
self.config.parser.print_help()
sys.exit(1)
Expand Down
8 changes: 4 additions & 4 deletions test/unit/module/config/test_config_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,18 @@ def test_config_expand_paths(self, yaml_mock):
)

@patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
def test_config_expand_paths_failure(self, yaml_mock):
def test_config_expand_paths_nomatch(self, yaml_mock):
"""Test precedence in"""

filename = "test/fixtures/templates/badpath/*.yaml"
filename = "test/fixtures/templates/nonexistant/*.yaml"
yaml_mock.side_effect = [
{"templates": ["test/fixtures/templates/badpath/*.yaml"]},
{"templates": [filename]},
{},
]
config = cfnlint.config.ConfigMixIn([])

# test defaults
self.assertEqual(config.templates, [str(Path(filename))])
self.assertEqual(config.templates, [])

@patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
def test_config_expand_ignore_templates(self, yaml_mock):
Expand Down

0 comments on commit e2a6c83

Please sign in to comment.