Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parsing multiple files #331

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ There are IDE plugins available to get direct linter feedback from you favorite
* [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=kddejong.vscode-cfn-lint)
* [IntelliJ IDEA](https://plugins.jetbrains.com/plugin/10973-cfn-lint/update/48247)

## Basic Usage
- `cfn-lint template.yaml`
- `cfn-lint -t template.yaml`

##### Lint multiple files
- `cfn-lint template1.yaml template2.yaml`
- `cfn-lint -t template1.yaml template2.yaml`
- `cfn-lint path/*.yaml`

##### Specifying the template with other parameters
- `cfn-lint -r us-east-1 ap-south-1 -- template.yaml`
- `cfn-lint -r us-east-1 ap-south-1 -t template.yaml`

## Configuration

### Command Line
Expand Down Expand Up @@ -78,15 +91,6 @@ Metadata:
### Precedence
cfn-lint applies the configuration from the CloudFormation Metadata first and then overrides those values with anything specified in the CLI.

## Examples
### Basic usage
`cfn-lint --template template.yaml`

### Test a template based on multiple regions
`cfn-lint --regions us-east-1 ap-south-1 --template template.yaml`

> E3001 Invalid Type AWS::Batch::ComputeEnvironment for resource testBatch in ap-south-1

### Getting Started Guides
There are [getting started guides](/docs/getting_started) available in the documentation section to help with integrating `cfn-lint` or creating rules.

Expand Down
23 changes: 16 additions & 7 deletions src/cfnlint/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@

def main():
"""Main function"""
(args, filename, template, rules, formatter) = cfnlint.core.get_template_args_rules(sys.argv[1:])

return(
cfnlint.core.run_cli(
filename, template, rules,
args.regions,
args.override_spec, formatter))
(args, filenames, formatter) = cfnlint.core.get_args_filenames(sys.argv[1:])
matches = []
for filename in filenames:
LOGGER.debug('Begin linting of file: %s', str(filename))
(template, rules, template_matches) = cfnlint.core.get_template_rules(filename, args)
if not template_matches:
matches.extend(
cfnlint.core.run_cli(
filename, template, rules,
args.regions, args.override_spec))
else:
matches.extend(template_matches)
LOGGER.debug('Completed linting of file: %s', str(filename))

formatter.print_matches(matches)
return cfnlint.core.get_exit_code(matches)


if __name__ == '__main__':
Expand Down
83 changes: 48 additions & 35 deletions src/cfnlint/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,13 @@ def error(self, message):
self.exit(32, '%s: error: %s\n' % (self.prog, message))


def run_cli(filename, template, rules, regions, override_spec, formatter):
def run_cli(filename, template, rules, regions, override_spec):
"""Process args and run"""

if override_spec:
cfnlint.helpers.override_specs(override_spec)

matches = run_checks(filename, template, rules, regions)

formatter.print_matches(matches)

return get_exit_code(matches)
return run_checks(filename, template, rules, regions)


def get_exit_code(matches):
Expand Down Expand Up @@ -85,12 +81,21 @@ def comma_separated_arg(string):
return string.split(',')


def space_separated_arg(string):
""" Split a comma separated string """
return string.split(' ')


class ExtendAction(argparse.Action):
"""Support argument types that are lists and can be specified multiple times."""
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest, [])
items = getattr(namespace, self.dest)
items = [] if items is None else items
for value in values:
items.extend(value)
if isinstance(value, list):
items.extend(value)
else:
items.append(value)
setattr(namespace, self.dest, items)


Expand All @@ -104,10 +109,10 @@ def create_parser():

# Alllow the template to be passes as an optional or a positional argument
standard.add_argument(
'template', nargs='?', help='The CloudFormation template to be linted')
'templates', metavar='TEMPLATE', nargs='*', help='The CloudFormation template to be linted')
standard.add_argument(
'-t', '--template', metavar='TEMPLATE', dest='template_alt', help='The CloudFormation template to be linted')

'-t', '--template', metavar='TEMPLATE', dest='template_alt',
cmmeyer marked this conversation as resolved.
Show resolved Hide resolved
help='The CloudFormation template to be linted', nargs='+', default=[], action='extend')
standard.add_argument(
'-b', '--ignore-bad-template', help='Ignore failures with Bad template',
action='store_true'
Expand Down Expand Up @@ -192,11 +197,9 @@ def get_rules(rulesdir, ignore_rules):
return rules


def get_template_args_rules(cli_args):
def get_args_filenames(cli_args):
""" Get Template Configuration items and set them as default values"""
template = {}
parser = create_parser()

args, _ = parser.parse_known_args(cli_args)

configure_logging(args.debug)
Expand All @@ -206,27 +209,18 @@ def get_template_args_rules(cli_args):

# Filename can be speficied as positional or optional argument. Positional
# is leading
if args.template:
filename = args.template
if args.templates:
filenames = args.templates
elif args.template_alt:
filename = args.template_alt
filenames = args.template_alt
else:
filename = None
filenames = None

if filename:
(template, matches) = cfnlint.decode.decode(filename, args.ignore_bad_template)
# if only one is specified convert it to array
if isinstance(filenames, six.string_types):
filenames = [filenames]

if matches:
formatter.print_matches(matches)
sys.exit(get_exit_code(matches))

# If the template has cfn-lint Metadata but the same options are set on the command-
# line, ignore the template's configuration. This works because these are all appends
# that have default values of empty arrays or none. The only one that really doesn't
# work is ignore_bad_template but you can't override that back to false at this point.
for section, values in get_default_args(template).items():
if not getattr(args, section):
setattr(args, section, values)
rules = cfnlint.core.get_rules(args.append_rules, args.ignore_checks)

# Set default regions if none are specified.
if not args.regions:
Expand All @@ -236,8 +230,6 @@ def get_template_args_rules(cli_args):
cfnlint.maintenance.update_resource_specs()
exit(0)

rules = cfnlint.core.get_rules(args.append_rules, args.ignore_checks)

if args.update_documentation:
cfnlint.maintenance.update_documentation(rules)
exit(0)
Expand All @@ -246,12 +238,33 @@ def get_template_args_rules(cli_args):
print(rules)
exit(0)

if not filename:
if not filenames:
# Not specified, print the help
parser.print_help()
exit(1)

return(args, filename, template, rules, formatter)
return(args, filenames, formatter)


def get_template_rules(filename, args):
""" Get Template Configuration items and set them as default values"""

(template, matches) = cfnlint.decode.decode(filename, args.ignore_bad_template)

if matches:
return(template, [], matches)

# If the template has cfn-lint Metadata but the same options are set on the command-
# line, ignore the template's configuration. This works because these are all appends
# that have default values of empty arrays or none. The only one that really doesn't
# work is ignore_bad_template but you can't override that back to false at this point.
for section, values in get_default_args(template).items():
if not getattr(args, section):
setattr(args, section, values)

rules = cfnlint.core.get_rules(args.append_rules, args.ignore_checks)

return(template, rules, [])


def get_default_args(template):
Expand Down
18 changes: 14 additions & 4 deletions src/cfnlint/decode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ def decode(filename, ignore_bad_template):
except IOError as e:
if e.errno == 2:
LOGGER.error('Template file not found: %s', filename)
sys.exit(1)
matches.append(create_match_file_error(filename, 'Template file not found: %s' % filename))
elif e.errno == 21:
LOGGER.error('Template references a directory, not a file: %s', filename)
sys.exit(1)
matches.append(create_match_file_error(filename, 'Template references a directory, not a file: %s' % filename))
elif e.errno == 13:
LOGGER.error('Permission denied when accessing template file: %s', filename)
sys.exit(1)
matches.append(create_match_file_error(filename, 'Permission denied when accessing template file: %s' % filename))

if matches:
return(None, matches)
except cfnlint.decode.cfn_yaml.CfnParseError as err:
err.match.Filename = filename
matches = [err.match]
Expand All @@ -71,7 +74,7 @@ def decode(filename, ignore_bad_template):
else:
LOGGER.error('Template %s is malformed: %s', filename, err.problem)
LOGGER.error('Tried to parse %s as JSON but got error: %s', filename, str(json_err))
sys.exit(1)
return(None, [create_match_file_error(filename, 'Tried to parse %s as JSON but got error: %s' % (filename, str(json_err)))])
else:
matches = [create_match_yaml_parser_error(err, filename)]

Expand All @@ -91,6 +94,13 @@ def create_match_yaml_parser_error(parser_error, filename):
cfnlint.ParseError(), message=msg)


def create_match_file_error(filename, msg):
"""Create a Match for a parser error"""
return cfnlint.Match(
linenumber=1, columnnumber=1, linenumberend=1, columnnumberend=2,
filename=filename, rule=cfnlint.ParseError(), message=msg)


def create_match_json_parser_error(parser_error, filename):
"""Create a Match for a parser error"""
if sys.version_info[0] == 3:
Expand Down
24 changes: 21 additions & 3 deletions test/module/core/test_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,28 @@ def test_create_parser(self):

parser = cfnlint.core.create_parser()
args = parser.parse_args([
'--template', 'test.yaml', '--ignore-bad-template',
'-t', 'test.yaml', '--ignore-bad-template',
'--format', 'quiet', '--debug'])
self.assertEqual(args.template, None)
self.assertEqual(args.template_alt, 'test.yaml')
self.assertEqual(args.templates, [])
self.assertEqual(args.template_alt, ['test.yaml'])
self.assertEqual(args.ignore_bad_template, True)
self.assertEqual(args.format, 'quiet')
self.assertEqual(args.debug, True)

def test_create_parser_default_param(self):
"""Test success run"""

parser = cfnlint.core.create_parser()
args = parser.parse_args([
'--regions', 'us-east-1', 'us-west-2', '--', 'template1.yaml', 'template2.yaml'])
self.assertEqual(args.templates, ['template1.yaml', 'template2.yaml'])
self.assertEqual(args.template_alt, [])
self.assertEqual(args.regions, ['us-east-1', 'us-west-2'])

def test_create_parser_exend(self):
"""Test success run"""

parser = cfnlint.core.create_parser()
args = parser.parse_args(['-t', 'template1.yaml', '-t', 'template2.yaml'])
self.assertEqual(args.templates, [])
self.assertEqual(args.template_alt, ['template1.yaml', 'template2.yaml'])
21 changes: 14 additions & 7 deletions test/module/core/test_run_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,30 @@ def test_good_template(self):
"""Test success run"""

filename = 'fixtures/templates/good/generic.yaml'
(args, filename, template, rules, _) = cfnlint.core.get_template_args_rules([
(args, filenames, _) = cfnlint.core.get_args_filenames([
'--template', filename])

results = cfnlint.core.run_checks(
filename, template, rules, ['us-east-1'])
results = []
for filename in filenames:
(template, rules, _) = cfnlint.core.get_template_rules(filename, args)
results.extend(
cfnlint.core.run_checks(
filename, template, rules, ['us-east-1']))

assert(results == [])

def test_bad_template(self):
"""Test bad template"""

filename = 'fixtures/templates/quickstart/nat-instance.json'
(args, filename, template, rules, _) = cfnlint.core.get_template_args_rules([
(args, filenames, _) = cfnlint.core.get_args_filenames([
'--template', filename])

results = cfnlint.core.run_checks(
filename, template, rules, ['us-east-1'])
results = []
for filename in filenames:
(template, rules, _) = cfnlint.core.get_template_rules(filename, args)
results.extend(
cfnlint.core.run_checks(
filename, template, rules, ['us-east-1']))

assert(results[0].rule.id == 'W2506')
assert(results[1].rule.id == 'W2001')
Loading