refactor: drop default standards version and rename to rules (#752)

BREAKING CHANGE: The option to define a `Standards` version has been removed. Every new rule that is added on upcoming releases is activated by default and will also create errors if triggered. The behavior of rules can be controlled by the existing `rules.exclude_filter` or `rules.warning_filter` options.

BREAKING CHANGE: The option `rules.buildin` has been renamed to `rules.builtin`.

BREAKING CHANGE: The option `rules.standards` has been renamed to `rules.dir`.

BREAKING CHANGE: The option `rules.filter` has been renamed to `rules.include_filter`.
This commit is contained in:
Robert Kaussow 2024-01-25 21:40:15 +01:00 committed by GitHub
parent d360de2125
commit 2df48598ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 201 additions and 325 deletions

View File

@ -7,8 +7,8 @@ import sys
from ansiblelater import LOG, __version__, logger
from ansiblelater.candidate import Candidate
from ansiblelater.rule import SingleRules
from ansiblelater.settings import Settings
from ansiblelater.standard import SingleStandards
def main():
@ -22,33 +22,33 @@ def main():
parser.add_argument(
"-r",
"--rules-dir",
dest="rules.standards",
metavar="RULES",
dest="rules.dir",
metavar="DIR",
action="append",
help="directory of standard rules",
help="directory of rules",
)
parser.add_argument(
"-B",
"--no-buildin",
dest="rules.buildin",
"--no-builtin",
dest="rules.builtin",
action="store_false",
help="disables build-in standard rules",
help="disables built-in rules",
)
parser.add_argument(
"-s",
"--standards",
dest="rules.filter",
metavar="FILTER",
"-i",
"--include-rules",
dest="rules.include_filter",
metavar="TAGS",
action="append",
help="limit standards to given ID's",
help="limit rules to given id/tags",
)
parser.add_argument(
"-x",
"--exclude-standards",
"--exclude-rules",
dest="rules.exclude_filter",
metavar="EXCLUDE_FILTER",
metavar="TAGS",
action="append",
help="exclude standards by given ID's",
help="exclude rules by given it/tags",
)
parser.add_argument(
"-v", dest="logging.level", action="append_const", const=-1, help="increase log level"
@ -65,7 +65,7 @@ def main():
config = settings.config
logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"])
SingleStandards(config["rules"]["standards"])
SingleRules(config["rules"]["dir"])
workers = max(multiprocessing.cpu_count() - 2, 2)
p = multiprocessing.Pool(workers)

View File

@ -3,14 +3,12 @@
import codecs
import copy
import os
import re
from ansible.plugins.loader import module_loader
from packaging.version import Version
from ansiblelater import LOG, utils
from ansiblelater import LOG
from ansiblelater.logger import flag_extra
from ansiblelater.standard import SingleStandards, StandardBase
from ansiblelater.rule import RuleBase, SingleRules
class Candidate:
@ -21,7 +19,7 @@ class Candidate:
bundled with necessary meta informations for rule processing.
"""
def __init__(self, filename, settings={}, standards=[]): # noqa
def __init__(self, filename, settings={}, rules=[]): # noqa
self.path = filename
self.binary = False
self.vault = False
@ -37,163 +35,114 @@ class Candidate:
except UnicodeDecodeError:
self.binary = True
def _get_version(self):
name = type(self).__name__
path = self.path
version = None
config_version = self.config["rules"]["version"].strip()
if config_version:
version_config_re = re.compile(r"([\d.]+)")
match = version_config_re.match(config_version)
if match:
version = match.group(1)
if not self.binary:
if isinstance(self, RoleFile):
parentdir = os.path.dirname(os.path.abspath(self.path))
while parentdir != os.path.dirname(parentdir):
meta_file = os.path.join(parentdir, "meta", "main.yml")
if os.path.exists(meta_file):
path = meta_file
break
parentdir = os.path.dirname(parentdir)
version_file_re = re.compile(r"^# Standards:\s*([\d.]+)")
with codecs.open(path, mode="rb", encoding="utf-8") as f:
for line in f:
match = version_file_re.match(line)
if match:
version = match.group(1)
if version:
LOG.info(f"{name} {path} declares standards version {version}")
return version
def _filter_standards(self):
target_standards = []
includes = self.config["rules"]["filter"]
def _filter_rules(self):
target_rules = []
includes = self.config["rules"]["include_filter"]
excludes = self.config["rules"]["exclude_filter"]
if len(includes) == 0:
includes = [s.sid for s in self.standards]
includes = [s.sid for s in self.rules]
for standard in self.standards:
if standard.sid in includes and standard.sid not in excludes:
target_standards.append(standard)
for rule in self.rules:
if rule.sid in includes and rule.sid not in excludes:
target_rules.append(rule)
return target_standards
return target_rules
def review(self):
errors = 0
self.standards = SingleStandards(self.config["rules"]["standards"]).rules
self.version_config = self._get_version()
self.version = self.version_config or utils.standards_latest(self.standards)
self.rules = SingleRules(self.config["rules"]["dir"]).rules
for standard in self._filter_standards():
if type(self).__name__.lower() not in standard.types:
for rule in self._filter_rules():
if type(self).__name__.lower() not in rule.types:
continue
result = standard.check(self, self.config)
result = rule.check(self, self.config)
if not result:
LOG.error(
f"Standard '{standard.sid}' returns an empty result object. Check failed!"
)
LOG.error(f"rule '{rule.sid}' returns an empty result object. Check failed!")
continue
labels = {
"tag": "review",
"standard": standard.description,
"rule": rule.description,
"file": self.path,
"passed": True,
}
if standard.sid and standard.sid.strip():
labels["sid"] = standard.sid
if rule.sid and rule.sid.strip():
labels["sid"] = rule.sid
for err in result.errors:
err_labels = copy.copy(labels)
err_labels["passed"] = False
sid = self._format_id(standard.sid)
sid = self._format_id(rule.sid)
path = self.path
description = standard.description
description = rule.description
if isinstance(err, StandardBase.Error):
if isinstance(err, RuleBase.Error):
err_labels.update(err.to_dict())
if not standard.version:
LOG.warning(
f"{sid}Best practice '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels),
)
elif Version(standard.version) > Version(self.version):
LOG.warning(
f"{sid}Future standard '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels),
)
else:
msg = f"{sid}Standard '{description}' not met:\n{path}:{err}"
msg = f"{sid}rule '{description}' not met:\n{path}:{err}"
if standard.sid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1
else:
LOG.warning(msg, extra=flag_extra(err_labels))
if rule.sid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1
else:
LOG.warning(msg, extra=flag_extra(err_labels))
return errors
@staticmethod
def classify(filename, settings={}, standards=[]): # noqa
def classify(filename, settings={}, rules=[]): # noqa
parentdir = os.path.basename(os.path.dirname(filename))
basename = os.path.basename(filename)
ext = os.path.splitext(filename)[1][1:]
if parentdir in ["tasks"]:
return Task(filename, settings, standards)
return Task(filename, settings, rules)
if parentdir in ["handlers"]:
return Handler(filename, settings, standards)
return Handler(filename, settings, rules)
if parentdir in ["vars", "defaults"]:
return RoleVars(filename, settings, standards)
return RoleVars(filename, settings, rules)
if "group_vars" in filename.split(os.sep):
return GroupVars(filename, settings, standards)
return GroupVars(filename, settings, rules)
if "host_vars" in filename.split(os.sep):
return HostVars(filename, settings, standards)
return HostVars(filename, settings, rules)
if parentdir in ["meta"] and "main" in basename:
return Meta(filename, settings, standards)
return Meta(filename, settings, rules)
if parentdir in ["meta"] and "argument_specs" in basename:
return ArgumentSpecs(filename, settings, standards)
return ArgumentSpecs(filename, settings, rules)
if parentdir in [
"library",
"lookup_plugins",
"callback_plugins",
"filter_plugins",
] or filename.endswith(".py"):
return Code(filename, settings, standards)
return Code(filename, settings, rules)
if basename == "inventory" or basename == "hosts" or parentdir in ["inventories"]:
return Inventory(filename, settings, standards)
return Inventory(filename, settings, rules)
if "rolesfile" in basename or ("requirements" in basename and ext in ["yaml", "yml"]):
return Rolesfile(filename, settings, standards)
return Rolesfile(filename, settings, rules)
if "Makefile" in basename:
return Makefile(filename, settings, standards)
return Makefile(filename, settings, rules)
if "templates" in filename.split(os.sep) or basename.endswith(".j2"):
return Template(filename, settings, standards)
return Template(filename, settings, rules)
if "files" in filename.split(os.sep):
return File(filename, settings, standards)
return File(filename, settings, rules)
if basename.endswith(".yml") or basename.endswith(".yaml"):
return Playbook(filename, settings, standards)
return Playbook(filename, settings, rules)
if "README" in basename:
return Doc(filename, settings, standards)
return Doc(filename, settings, rules)
return None
def _format_id(self, standard_id):
sid = standard_id.strip()
def _format_id(self, rule_id):
sid = rule_id.strip()
if sid:
standard_id = f"[{sid}] "
rule_id = f"[{sid}] "
return standard_id
return rule_id
def __repr__(self):
return f"{type(self).__name__} ({self.path})"
@ -205,8 +154,8 @@ class Candidate:
class RoleFile(Candidate):
"""Object classified as Ansible role file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
parentdir = os.path.dirname(os.path.abspath(filename))
while parentdir != os.path.dirname(parentdir):
@ -226,16 +175,16 @@ class Playbook(Candidate):
class Task(RoleFile):
"""Object classified as Ansible task file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filetype = "tasks"
class Handler(RoleFile):
"""Object classified as Ansible handler file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filetype = "handlers"

View File

@ -1,4 +1,4 @@
"""Standard definition."""
"""Rule definition."""
import copy
import importlib
@ -27,22 +27,21 @@ from ansiblelater.utils.yamlhelper import (
)
class StandardMeta(type):
class RuleMeta(type):
def __call__(cls, *args):
mcls = type.__call__(cls, *args)
mcls.sid = cls.sid
mcls.description = getattr(cls, "description", "__unknown__")
mcls.helptext = getattr(cls, "helptext", "")
mcls.version = getattr(cls, "version", None)
mcls.types = getattr(cls, "types", [])
return mcls
class StandardExtendedMeta(StandardMeta, ABCMeta):
class RuleExtendedMeta(RuleMeta, ABCMeta):
pass
class StandardBase(metaclass=StandardExtendedMeta):
class RuleBase(metaclass=RuleExtendedMeta):
SHELL_PIPE_CHARS = "&|<>;$\n*[]{}?"
@property
@ -55,7 +54,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
pass
def __repr__(self):
return f"Standard: {self.description} (version: {self.version}, types: {self.types})"
return f"Rule: {self.description} (types: {self.types})"
@staticmethod
def get_tasks(candidate, settings): # noqa
@ -69,11 +68,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -93,11 +92,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return tasks, errors
@ -115,11 +114,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -159,11 +158,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -184,11 +183,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -210,7 +209,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
content = yaml.safe_load(f)
except yaml.YAMLError as e:
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
@ -224,14 +223,14 @@ class StandardBase(metaclass=StandardExtendedMeta):
try:
with open(candidate.path, encoding="utf-8") as f:
for problem in linter.run(f, YamlLintConfig(options)):
errors.append(StandardBase.Error(problem.line, problem.desc))
errors.append(RuleBase.Error(problem.line, problem.desc))
except yaml.YAMLError as e:
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except (TypeError, ValueError) as e:
errors.append(StandardBase.Error(None, f"yamllint error: {e}"))
errors.append(RuleBase.Error(None, f"yamllint error: {e}"))
candidate.faulty = True
return errors
@ -302,7 +301,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
return "\n".join([f"{self.candidate}:{error}" for error in self.errors])
class StandardLoader:
class RulesLoader:
def __init__(self, source):
self.rules = []
@ -331,10 +330,7 @@ class StandardLoader:
def _is_plugin(self, obj):
return (
inspect.isclass(obj)
and issubclass(obj, StandardBase)
and obj is not StandardBase
and not None
inspect.isclass(obj) and issubclass(obj, RuleBase) and obj is not RuleBase and not None
)
def validate(self):
@ -343,11 +339,11 @@ class StandardLoader:
all_std = len(normalized_std)
if all_std != unique_std:
sysexit_with_message(
"Detect duplicate ID's in standards definition. Please use unique ID's only."
"Found duplicate tags in rules definition. Please use unique tags only."
)
class SingleStandards(StandardLoader, metaclass=Singleton):
class SingleRules(RulesLoader, metaclass=Singleton):
"""Singleton config class."""
pass

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckBecomeUser(StandardBase):
class CheckBecomeUser(RuleBase):
sid = "ANSIBLE0015"
description = "Become should be combined with become_user"
helptext = "the task has `become` enabled but `become_user` is missing"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
from ansiblelater.utils import count_spaces
class CheckBracesSpaces(StandardBase):
class CheckBracesSpaces(RuleBase):
sid = "ANSIBLE0004"
description = "YAML should use consistent number of spaces around variables"
helptext = "no suitable numbers of spaces (min: {min} max: {max})"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckChangedInWhen(StandardBase):
class CheckChangedInWhen(RuleBase):
sid = "ANSIBLE0026"
description = "Use handlers instead of `when: changed`"
helptext = "tasks using `when: result.changed` setting are effectively acting as a handler"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandHasChanges(StandardBase):
class CheckCommandHasChanges(RuleBase):
sid = "ANSIBLE0011"
description = "Commands should be idempotent"
helptext = (
"commands should only read while using `changed_when` or try to be "
"idempotent while using controls like `creates`, `removes` or `when`"
)
version = "0.1"
types = ["playbook", "task"]
def check(self, candidate, settings):

View File

@ -20,14 +20,13 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandInsteadOfArgument(StandardBase):
class CheckCommandInsteadOfArgument(RuleBase):
sid = "ANSIBLE0017"
description = "Commands should not be used in place of module arguments"
helptext = "{exec} used in place of file modules argument {arg}"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandInsteadOfModule(StandardBase):
class CheckCommandInsteadOfModule(RuleBase):
sid = "ANSIBLE0008"
description = "Commands should not be used in place of modules"
helptext = "{exec} command used in place of {module} module"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCompareToEmptyString(StandardBase):
class CheckCompareToEmptyString(RuleBase):
sid = "ANSIBLE0012"
description = 'Don\'t compare to empty string ""'
helptext = "use `when: var` rather than `when: var !=` (or conversely `when: not var`)"
version = "0.1"
types = ["playbook", "task", "handler", "template"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCompareToLiteralBool(StandardBase):
class CheckCompareToLiteralBool(RuleBase):
sid = "ANSIBLE0013"
description = "Don't compare to True or False"
helptext = "use `when: var` rather than `when: var == True` (or conversely `when: not var`)"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckDeprecated(StandardBase):
class CheckDeprecated(RuleBase):
sid = "ANSIBLE9999"
description = "Deprecated features should not be used"
helptext = "'{old}' is deprecated and should not be used anymore. Use '{new}' instead."
version = "0.1"
helptext = "`{old}` is deprecated and should not be used anymore. Use `{new}` instead."
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -20,18 +20,17 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
from ansiblelater.utils import has_glob, has_jinja
class CheckDeprecatedBareVars(StandardBase):
class CheckDeprecatedBareVars(RuleBase):
sid = "ANSIBLE0027"
description = "Deprecated bare variables in loops must not be used"
helptext = (
"bare var '{barevar}' in '{loop_type}' must use full var syntax '{{{{ {barevar} }}}}' "
"or be converted to a list"
)
version = "0.3"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -19,17 +19,16 @@
# THE SOFTWARE.
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilePermissionMissing(StandardBase):
class CheckFilePermissionMissing(RuleBase):
sid = "ANSIBLE0018"
description = "File permissions unset or incorrect"
helptext = (
"`mode` parameter should set permissions explicitly (e.g. `mode: 0644`) "
"to avoid unexpected file permissions"
)
version = "0.2"
types = ["playbook", "task", "handler"]
_modules = {

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilePermissionOctal(StandardBase):
class CheckFilePermissionOctal(RuleBase):
sid = "ANSIBLE0019"
description = "Octal file permissions must contain leading zero or be a string"
helptext = "numeric file permissions without leading zero can behave in unexpected ways"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilterSeparation(StandardBase):
class CheckFilterSeparation(RuleBase):
sid = "ANSIBLE0016"
description = "Jinja2 filters should be separated with spaces"
helptext = "no suitable numbers of spaces (required: 1)"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings):

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckGitHasVersion(StandardBase):
class CheckGitHasVersion(RuleBase):
sid = "ANSIBLE0020"
description = "Git checkouts should use explicit version"
helptext = "git checkouts should point to an explicit commit or tag, not `latest`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckInstallUseLatest(StandardBase):
class CheckInstallUseLatest(RuleBase):
sid = "ANSIBLE0009"
description = "Package installs should use present, not latest"
helptext = "package installs should use `state=present` with or without a version"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckLiteralBoolFormat(StandardBase):
class CheckLiteralBoolFormat(RuleBase):
sid = "ANSIBLE0014"
description = "Literal bools should be consistent"
helptext = "literal bools should be written as `{bools}`"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckLocalAction(StandardBase):
class CheckLocalAction(RuleBase):
sid = "ANSIBLE0024"
description = "Don't use local_action"
helptext = "`delegate_to: localhost` should be used instead of `local_action`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
# Copyright (c) 2018, Ansible Project
from nested_lookup import nested_lookup
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckMetaChangeFromDefault(StandardBase):
class CheckMetaChangeFromDefault(RuleBase):
sid = "ANSIBLE0021"
description = "Roles meta/main.yml default values should be changed"
helptext = "meta/main.yml default values should be changed for: `{field}`"
version = "0.2"
types = ["meta"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from nested_lookup import nested_lookup
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckMetaMain(StandardBase):
class CheckMetaMain(RuleBase):
sid = "ANSIBLE0002"
description = "Roles must contain suitable meta/main.yml"
helptext = "file should contain `{key}` key"
version = "0.1"
types = ["meta"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNameFormat(StandardBase):
class CheckNameFormat(RuleBase):
sid = "ANSIBLE0007"
description = "Name of tasks and handlers must be formatted"
helptext = "name '{name}' should start with uppercase"
version = "0.1"
helptext = "name `{name}` should start with uppercase"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNamedTask(StandardBase):
class CheckNamedTask(RuleBase):
sid = "ANSIBLE0006"
description = "Tasks and handlers must be named"
helptext = "module '{module}' used without or empty `name` attribute"
version = "0.1"
helptext = "module `{module}` used without or empty `name` attribute"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNativeYaml(StandardBase):
class CheckNativeYaml(RuleBase):
sid = "LINT0008"
description = "Use YAML format for tasks and handlers rather than key=value"
helptext = "task arguments appear to be in key value rather than YAML format"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -21,17 +21,16 @@
# THE SOFTWARE.
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNestedJinja(StandardBase):
class CheckNestedJinja(RuleBase):
sid = "ANSIBLE0023"
description = "Don't use nested Jinja2 pattern"
helptext = (
"there should not be any nested jinja pattern "
"like `{{ list_one + {{ list_two | max }} }}`"
)
version = "0.2"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckRelativeRolePaths(StandardBase):
class CheckRelativeRolePaths(RuleBase):
sid = "ANSIBLE0025"
description = "Don't use a relative path in a role"
helptext = "`copy` and `template` modules don't need relative path for `src`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from ansible.parsing.yaml.objects import AnsibleMapping
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckScmInSrc(StandardBase):
class CheckScmInSrc(RuleBase):
sid = "ANSIBLE0005"
description = "Use `scm:` key rather than `src: scm+url`"
helptext = "usage of `src: scm+url` not recommended"
version = "0.1"
types = ["rolesfile"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckShellInsteadCommand(StandardBase):
class CheckShellInsteadCommand(RuleBase):
sid = "ANSIBLE0010"
description = "Shell should only be used when essential"
helptext = "shell should only be used when piping, redirecting or chaining commands"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
import re
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckTaskSeparation(StandardBase):
class CheckTaskSeparation(RuleBase):
sid = "ANSIBLE0001"
description = "Single tasks should be separated by empty line"
helptext = "missing task separation (required: 1 empty line)"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckUniqueNamedTask(StandardBase):
class CheckUniqueNamedTask(RuleBase):
sid = "ANSIBLE0003"
description = "Tasks and handlers must be uniquely named within a single file"
helptext = "name '{name}' appears multiple times"
version = "0.1"
helptext = "name `{name}` appears multiple times"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,16 +0,0 @@
from ansiblelater.standard import StandardBase
class CheckVersion(StandardBase):
sid = "ANSIBLE9998"
description = "Standards version should be pinned"
helptext = "Standards version not set. Using latest standards version {version}"
types = ["task", "handler", "rolevars", "meta", "template", "file", "playbook"]
def check(self, candidate, settings): # noqa
errors = []
if not candidate.version_config:
errors.append(self.Error(None, self.helptext.format(version=candidate.version)))
return self.Result(candidate.path, errors)

View File

@ -1,13 +1,12 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckWhenFormat(StandardBase):
class CheckWhenFormat(RuleBase):
sid = "ANSIBLE0022"
description = "Don't use Jinja2 in when"
helptext = (
"`when` is a raw Jinja2 expression, redundant {{ }} " "should be removed from variable(s)"
)
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlColons(StandardBase):
class CheckYamlColons(RuleBase):
sid = "LINT0005"
description = "YAML should use consistent number of spaces around colons"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlDocumentEnd(StandardBase):
class CheckYamlDocumentEnd(RuleBase):
sid = "LINT0009"
description = "YAML should contain document end marker"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlDocumentStart(StandardBase):
class CheckYamlDocumentStart(RuleBase):
sid = "LINT0004"
description = "YAML should contain document start marker"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlEmptyLines(StandardBase):
class CheckYamlEmptyLines(RuleBase):
sid = "LINT0001"
description = "YAML should not contain unnecessarily empty lines"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlFile(StandardBase):
class CheckYamlFile(RuleBase):
sid = "LINT0006"
description = "Roles file should be in yaml format"
helptext = "file does not have a .yml extension"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlHasContent(StandardBase):
class CheckYamlHasContent(RuleBase):
sid = "LINT0007"
description = "Files should contain useful content"
helptext = "the file appears to have no useful content"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "defaults", "meta"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlHyphens(StandardBase):
class CheckYamlHyphens(RuleBase):
sid = "LINT0003"
description = "YAML should use consistent number of spaces after hyphens"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlIndent(StandardBase):
class CheckYamlIndent(RuleBase):
sid = "LINT0002"
description = "YAML should not contain unnecessarily empty lines"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):

View File

@ -104,13 +104,13 @@ class Settings:
if f not in defaults["ansible"]["custom_modules"]:
defaults["ansible"]["custom_modules"].append(f)
if defaults["rules"]["buildin"]:
defaults["rules"]["standards"].append(
if defaults["rules"]["builtin"]:
defaults["rules"]["dir"].append(
os.path.join(resource_filename("ansiblelater", "rules"))
)
defaults["rules"]["standards"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["standards"]
defaults["rules"]["dir"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["dir"]
]
return defaults
@ -118,9 +118,9 @@ class Settings:
def _get_defaults(self):
defaults = {
"rules": {
"buildin": True,
"standards": [],
"filter": [],
"builtin": True,
"dir": [],
"include_filter": [],
"exclude_filter": [],
"warning_filter": [
"ANSIBLE9999",
@ -128,7 +128,6 @@ class Settings:
],
"ignore_dotfiles": True,
"exclude_files": [],
"version": "",
},
"logging": {
"level": "WARNING",

View File

@ -6,7 +6,6 @@ import sys
from contextlib import suppress
import yaml
from packaging.version import Version
from ansiblelater import logger
@ -35,12 +34,6 @@ def count_spaces(c_string):
return (leading_spaces, trailing_spaces)
def standards_latest(standards):
return max(
[standard.version for standard in standards if standard.version] or ["0.1"], key=Version
)
def lines_ranges(lines_spec):
if not lines_spec:
return None

View File

@ -1,18 +1,17 @@
---
title: Minimal standard checks
title: Write a rule
---
A typical standards check will look like:
A typical rule check will look like:
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< highlight Python "linenos=table" >}}
class CheckBecomeUser(StandardBase):
class CheckBecomeUser(RuleBase):
sid = "ANSIBLE0015"
description = "Become should be combined with become_user"
helptext = "the task has `become` enabled but `become_user` is missing"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -8,28 +8,27 @@ You can get all available CLI options by running `ansible-later --help`:
<!-- spellchecker-disable -->
{{< highlight Shell "linenos=table" >}}
$ ansible-later --help
usage: ansible-later [-h] [-c CONFIG_FILE] [-r RULES.STANDARDS]
[-s RULES.FILTER] [-v] [-q] [--version]
[rules.files [rules.files ...]]
usage: ansible-later [-h] [-c CONFIG] [-r DIR] [-B] [-i TAGS] [-x TAGS] [-v] [-q] [-V] [rules.files ...]
Validate Ansible files against best practice guideline
positional arguments:
rules.files
optional arguments:
options:
-h, --help show this help message and exit
-c CONFIG_FILE, --config CONFIG_FILE
location of configuration file
-r RULES.STANDARDS, --rules RULES.STANDARDS
location of standards rules
-s RULES.FILTER, --standards RULES.FILTER
limit standards to given ID's
-x RULES.EXCLUDE_FILTER, --exclude-standards RULES.EXCLUDE_FILTER
exclude standards by given ID's
-c CONFIG, --config CONFIG
path to configuration file
-r DIR, --rules-dir DIR
directory of rules
-B, --no-builtin disables built-in rules
-i TAGS, --include-rules TAGS
limit rules to given id/tags
-x TAGS, --exclude-rules TAGS
exclude rules by given it/tags
-v increase log level
-q decrease log level
--version show program's version number and exit
-V, --version show program's version number and exit
{{< /highlight >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->

View File

@ -58,8 +58,8 @@ logging:
# Global settings for all defined rules
rules:
# Disable build-in rules if required
buildin: True
# Disable built-in rules if required
builtin: True
# List of files to exclude
exclude_files: []
@ -75,8 +75,7 @@ rules:
exclude_filter: []
# List of rule ID's that should be displayed as a warning instead of an error. By default,
# only rules whose version is higher than the current default version are marked as warnings.
# This list allows to degrade errors to warnings for each rule.
# no rules are marked as warnings. This list allows to degrade errors to warnings for each rule.
warning_filter:
- "ANSIBLE9999"
- "ANSIBLE9998"
@ -85,12 +84,8 @@ rules:
# You can disable this setting and handle dotfiles by yourself with `exclude_files`.
ignore_dotfiles: True
# List of directories to load standard rules from (defaults to build-in)
standards: []
# Standard version to use. Standard version set in a roles meta file
# or playbook will takes precedence.
version:
# List of directories to load rules from (defaults to built-in)
dir: []
# Block to control included yamllint rules.
# See https://yamllint.readthedocs.io/en/stable/rules.html

View File

@ -2,7 +2,7 @@
title: Included rules
---
Reviews are useless without some rules or standards to check against. ansible-later comes with a set of built-in checks, which are explained in the following table.
Reviews are useless without some rules to check against. `ansible-later` comes with a set of built-in checks, which are explained in the following table.
| Rule | ID | Description | Parameter |
| ----------------------------- | ----------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
@ -42,5 +42,4 @@ Reviews are useless without some rules or standards to check against. ansible-la
| CheckRelativeRolePaths | ANSIBLE0025 | Don't use a relative path in a role. | |
| CheckChangedInWhen | ANSIBLE0026 | Use handlers instead of `when: changed`. | |
| CheckChangedInWhen | ANSIBLE0027 | Deprecated bare variables in loops must not be used. | |
| CheckVersion | ANSIBLE9998 | Standards version should be pinned. | |
| CheckDeprecated | ANSIBLE9999 | Deprecated features of `ansible-later` should not be used. | |

View File

@ -23,5 +23,5 @@ main:
sub:
- name: Candidates
ref: "/build_rules/candidates"
- name: Standards checks
ref: "/build_rules/standards_check"
- name: Rules
ref: "/build_rules/rule"