Compare commits

..

No commits in common. "main" and "v3.4.3" have entirely different histories.
main ... v3.4.3

66 changed files with 1081 additions and 1274 deletions

View File

@ -18,8 +18,8 @@ HostVars
Rolesfile
Makefile
Jinja2
ANS([0-9]{3})
YML([0-9]{3})
ANSIBLE([0-9]{4})
LINT([0-9]{4})
SCM
bools
Check[A-Z].+

View File

@ -1,6 +1,7 @@
repository:
name: ansible-later
description: Another best practice scanner for Ansible roles and playbooks
homepage: https://ansible-later.geekdocs.de
topics: ansible, ansible-later, ansible-review, best practice
private: false

View File

@ -12,31 +12,22 @@ steps:
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: security-build
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: [build]
- name: dryrun
image: quay.io/thegeeklab/wp-docker-buildx:2
settings:
containerfile: Containerfile.multiarch
output: type=oci,dest=oci/${CI_REPO_NAME},tar=false
dry_run: true
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
- name: security-scan
image: docker.io/aquasec/trivy
depends_on: [security-build]
commands:
- trivy -v
- trivy image --input oci/${CI_REPO_NAME}
environment:
TRIVY_EXIT_CODE: "1"
TRIVY_IGNORE_UNFIXED: "true"
TRIVY_NO_PROGRESS: "true"
TRIVY_SEVERITY: HIGH,CRITICAL
TRIVY_TIMEOUT: 1m
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2
when:
- event: [pull_request]
- name: publish-dockerhub
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: [security-scan]
image: quay.io/thegeeklab/wp-docker-buildx:2
group: container
settings:
auto_tag: true
containerfile: Containerfile.multiarch
@ -56,8 +47,8 @@ steps:
- ${CI_REPO_DEFAULT_BRANCH}
- name: publish-quay
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: security-scan
image: quay.io/thegeeklab/wp-docker-buildx:2
group: container
settings:
auto_tag: true
containerfile: Containerfile.multiarch

View File

@ -40,11 +40,11 @@ steps:
- name: publish-pypi
image: docker.io/library/python:3.12
environment:
POETRY_HTTP_BASIC_PYPI_PASSWORD:
from_secret: pypi_password
POETRY_HTTP_BASIC_PYPI_USERNAME:
from_secret: pypi_username
secrets:
- source: pypi_password
target: POETRY_HTTP_BASIC_PYPI_PASSWORD
- source: pypi_username
target: POETRY_HTTP_BASIC_PYPI_USERNAME
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry publish -n

View File

@ -13,13 +13,13 @@ steps:
- name: markdownlint
image: quay.io/thegeeklab/markdownlint-cli
depends_on: [assets]
group: test
commands:
- markdownlint 'README.md' 'CONTRIBUTING.md'
- name: spellcheck
image: quay.io/thegeeklab/alpine-tools
depends_on: [assets]
group: test
commands:
- spellchecker --files 'docs/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls
environment:
@ -27,25 +27,24 @@ steps:
- name: link-validation
image: docker.io/lycheeverse/lychee
depends_on: [assets]
group: test
commands:
- lychee --no-progress --format detailed docs/content README.md
- name: build
image: quay.io/thegeeklab/hugo:0.136.5
depends_on: [link-validation]
image: quay.io/thegeeklab/hugo:0.121.2
commands:
- hugo --panicOnWarning -s docs/
- name: beautify
image: quay.io/thegeeklab/alpine-tools
depends_on: [build]
commands:
- html-beautify -r -f 'docs/public/**/*.html'
environment:
FORCE_COLOR: "true"
- name: publish
image: quay.io/thegeeklab/wp-s3-action
depends_on: [beautify]
settings:
access_key:
from_secret: s3_access_key
@ -67,12 +66,12 @@ steps:
- name: pushrm-dockerhub
image: docker.io/chko/docker-pushrm:1
depends_on: [publish]
secrets:
- source: docker_password
target: DOCKER_PASS
- source: docker_username
target: DOCKER_USER
environment:
DOCKER_PASS:
from_secret: docker_password
DOCKER_USER:
from_secret: docker_username
PUSHRM_FILE: README.md
PUSHRM_SHORT: Another best practice scanner for Ansible roles and playbooks
PUSHRM_TARGET: ${CI_REPO}
@ -84,10 +83,10 @@ steps:
- name: pushrm-quay
image: docker.io/chko/docker-pushrm:1
depends_on: [publish]
secrets:
- source: quay_token
target: APIKEY__QUAY_IO
environment:
APIKEY__QUAY_IO:
from_secret: quay_token
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/${CI_REPO}
when:

View File

@ -8,7 +8,6 @@ when:
steps:
- name: check-format
image: docker.io/library/python:3.12
depends_on: []
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
@ -18,10 +17,9 @@ steps:
- name: check-coding
image: docker.io/library/python:3.12
depends_on: []
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run ruff check ./${CI_REPO_NAME//-/}
- poetry run ruff ./${CI_REPO_NAME//-/}
environment:
PY_COLORS: "1"

View File

@ -13,12 +13,12 @@ steps:
settings:
homeserver:
from_secret: matrix_homeserver
room_id:
from_secret: matrix_room_id
user_id:
from_secret: matrix_user_id
access_token:
from_secret: matrix_access_token
password:
from_secret: matrix_password
roomid:
from_secret: matrix_roomid
username:
from_secret: matrix_username
when:
- status: [success, failure]

View File

@ -7,7 +7,7 @@ when:
variables:
- &pytest_base
depends_on: []
group: pytest
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core

View File

@ -1,4 +1,4 @@
FROM python:3.12-alpine@sha256:38e179a0f0436c97ecc76bcd378d7293ab3ee79e4b8c440fdc7113670cb6e204
FROM python:3.12-alpine@sha256:c793b92fd9e0e2a0b611756788a033d569ca864b733461c8fb30cfd14847dbcf
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>"

View File

@ -1,5 +1,5 @@
# renovate: datasource=github-releases depName=thegeeklab/hugo-geekdoc
THEME_VERSION := v1.2.1
THEME_VERSION := v0.44.0
THEME := hugo-geekdoc
BASEDIR := docs
THEMEDIR := $(BASEDIR)/themes

View File

@ -12,12 +12,22 @@ Another best practice scanner for Ansible roles and playbooks
[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/ansible-later)
[![License: MIT](https://img.shields.io/github/license/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/blob/main/LICENSE)
> **Discontinued:** This project is no longer maintained. Please use [ansible-lint](https://github.com/ansible-community/ansible-lint) instead.
ansible-later is a best practice scanner and linting tool. In most cases, if you write Ansible roles in a team, it helps to have a coding or best practice guideline in place. This will make Ansible roles more readable for all maintainers and can reduce the troubleshooting time. While ansible-later aims to be a fast and easy to use linting tool for your Ansible resources, it might not be that feature completed as required in some situations. If you need a more in-depth analysis you can take a look at [ansible-lint](https://github.com/ansible-community/ansible-lint).
ansible-later does **not** ensure that your role will work as expected. For deployment tests you can use other tools like [molecule](https://github.com/ansible/molecule).
You can find the full documentation at [https://ansible-later.geekdocs.de](https://ansible-later.geekdocs.de/).
## Community
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
- [GitHub Action](https://github.com/patrickjahns/ansible-later-action) by [@patrickjahns](https://github.com/patrickjahns)
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->
## Contributors
Special thanks to all [contributors](https://github.com/thegeeklab/ansible-later/graphs/contributors). If you would like to contribute,

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.dir",
metavar="DIR",
dest="rules.standards",
metavar="RULES",
action="append",
help="directory of rules",
help="directory of standard rules",
)
parser.add_argument(
"-B",
"--no-builtin",
dest="rules.builtin",
"--no-buildin",
dest="rules.buildin",
action="store_false",
help="disables built-in rules",
help="disables build-in standard rules",
)
parser.add_argument(
"-i",
"--include-rules",
dest="rules.include_filter",
metavar="TAGS",
"-s",
"--standards",
dest="rules.filter",
metavar="FILTER",
action="append",
help="limit rules to given id/tags",
help="limit standards to given ID's",
)
parser.add_argument(
"-x",
"--exclude-rules",
"--exclude-standards",
dest="rules.exclude_filter",
metavar="TAGS",
metavar="EXCLUDE_FILTER",
action="append",
help="exclude rules by given it/tags",
help="exclude standards by given ID's",
)
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"])
SingleRules(config["rules"]["dir"])
SingleStandards(config["rules"]["standards"])
workers = max(multiprocessing.cpu_count() - 2, 2)
p = multiprocessing.Pool(workers)

View File

@ -3,12 +3,14 @@
import codecs
import copy
import os
import re
from ansible.plugins.loader import module_loader
from packaging.version import Version
from ansiblelater import LOG
from ansiblelater import LOG, utils
from ansiblelater.logger import flag_extra
from ansiblelater.rule import RuleBase, SingleRules
from ansiblelater.standard import SingleStandards, StandardBase
class Candidate:
@ -19,12 +21,11 @@ class Candidate:
bundled with necessary meta informations for rule processing.
"""
def __init__(self, filename, settings={}, rules=[]): # noqa
def __init__(self, filename, settings={}, standards=[]): # noqa
self.path = filename
self.binary = False
self.vault = False
self.filemeta = type(self).__name__.lower()
self.kind = type(self).__name__.lower()
self.filetype = type(self).__name__.lower()
self.faulty = False
self.config = settings.config
self.settings = settings
@ -36,117 +37,166 @@ class Candidate:
except UnicodeDecodeError:
self.binary = True
def _filter_rules(self):
target_rules = []
includes = self.config["rules"]["include_filter"]
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"]
excludes = self.config["rules"]["exclude_filter"]
if len(includes) == 0:
includes = [s.rid for s in self.rules]
includes = [s.sid for s in self.standards]
for rule in self.rules:
if rule.rid in includes and rule.rid not in excludes:
target_rules.append(rule)
for standard in self.standards:
if standard.sid in includes and standard.sid not in excludes:
target_standards.append(standard)
return target_rules
return target_standards
def review(self):
errors = 0
self.rules = SingleRules(self.config["rules"]["dir"]).rules
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)
for rule in self._filter_rules():
if self.kind not in rule.types:
for standard in self._filter_standards():
if type(self).__name__.lower() not in standard.types:
continue
result = rule.check(self, self.config)
result = standard.check(self, self.config)
if not result:
LOG.error(f"rule '{rule.rid}' returns an empty result object. Check failed!")
LOG.error(
f"Standard '{standard.sid}' returns an empty result object. Check failed!"
)
continue
labels = {
"tag": "review",
"rule": rule.description,
"standard": standard.description,
"file": self.path,
"passed": True,
}
if rule.rid and rule.rid.strip():
labels["rid"] = rule.rid
if standard.sid and standard.sid.strip():
labels["sid"] = standard.sid
for err in result.errors:
err_labels = copy.copy(labels)
err_labels["passed"] = False
rid = self._format_id(rule.rid)
sid = self._format_id(standard.sid)
path = self.path
description = rule.description
description = standard.description
if isinstance(err, RuleBase.Error):
if isinstance(err, StandardBase.Error):
err_labels.update(err.to_dict())
msg = f"{rid}rule '{description}' not met:\n{path}:{err}"
if rule.rid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1
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:
LOG.warning(msg, extra=flag_extra(err_labels))
msg = f"{sid}Standard '{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))
return errors
@staticmethod
def classify(filename, settings={}, rules=[]): # noqa
def classify(filename, settings={}, standards=[]): # 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, rules)
return Task(filename, settings, standards)
if parentdir in ["handlers"]:
return Handler(filename, settings, rules)
return Handler(filename, settings, standards)
if parentdir in ["vars", "defaults"]:
return RoleVars(filename, settings, rules)
return RoleVars(filename, settings, standards)
if "group_vars" in filename.split(os.sep):
return GroupVars(filename, settings, rules)
return GroupVars(filename, settings, standards)
if "host_vars" in filename.split(os.sep):
return HostVars(filename, settings, rules)
return HostVars(filename, settings, standards)
if parentdir in ["meta"] and "main" in basename:
return Meta(filename, settings, rules)
return Meta(filename, settings, standards)
if parentdir in ["meta"] and "argument_specs" in basename:
return ArgumentSpecs(filename, settings, rules)
return ArgumentSpecs(filename, settings, standards)
if parentdir in [
"library",
"lookup_plugins",
"callback_plugins",
"filter_plugins",
] or filename.endswith(".py"):
return Code(filename, settings, rules)
return Code(filename, settings, standards)
if basename == "inventory" or basename == "hosts" or parentdir in ["inventories"]:
return Inventory(filename, settings, rules)
return Inventory(filename, settings, standards)
if "rolesfile" in basename or ("requirements" in basename and ext in ["yaml", "yml"]):
return Rolesfile(filename, settings, rules)
return Rolesfile(filename, settings, standards)
if "Makefile" in basename:
return Makefile(filename, settings, rules)
return Makefile(filename, settings, standards)
if "templates" in filename.split(os.sep) or basename.endswith(".j2"):
return Template(filename, settings, rules)
return Template(filename, settings, standards)
if "files" in filename.split(os.sep):
return File(filename, settings, rules)
return File(filename, settings, standards)
if basename.endswith(".yml") or basename.endswith(".yaml"):
return Playbook(filename, settings, rules)
return Playbook(filename, settings, standards)
if "README" in basename:
return Doc(filename, settings, rules)
return Doc(filename, settings, standards)
return None
def _format_id(self, rule_id):
rid = rule_id.strip()
if rid:
rule_id = f"[{rid}] "
def _format_id(self, standard_id):
sid = standard_id.strip()
if sid:
standard_id = f"[{sid}] "
return rule_id
return standard_id
def __repr__(self):
return f"{self.kind} ({self.path})"
return f"{type(self).__name__} ({self.path})"
def __getitem__(self, item):
return self.__dict__.get(item)
@ -155,8 +205,8 @@ class Candidate:
class RoleFile(Candidate):
"""Object classified as Ansible role file."""
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
parentdir = os.path.dirname(os.path.abspath(filename))
while parentdir != os.path.dirname(parentdir):
@ -176,17 +226,17 @@ class Playbook(Candidate):
class Task(RoleFile):
"""Object classified as Ansible task file."""
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filemeta = "tasks"
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
self.filetype = "tasks"
class Handler(RoleFile):
"""Object classified as Ansible handler file."""
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filemeta = "handlers"
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
self.filetype = "handlers"
class Vars(Candidate):

View File

@ -82,7 +82,7 @@ class LogFilter:
class MultilineFormatter(logging.Formatter):
"""Logging Formatter to reset color after newline characters."""
def format(self, record):
def format(self, record): # noqa
record.msg = record.msg.replace("\n", f"\n{colorama.Style.RESET_ALL}... ")
record.msg = record.msg + "\n"
return logging.Formatter.format(self, record)
@ -91,7 +91,7 @@ class MultilineFormatter(logging.Formatter):
class MultilineJsonFormatter(jsonlogger.JsonFormatter):
"""Logging Formatter to remove newline characters."""
def format(self, record):
def format(self, record): # noqa
record.msg = record.msg.replace("\n", " ")
return jsonlogger.JsonFormatter.format(self, record)

View File

@ -1,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckBecomeUser(RuleBase):
rid = "ANS115"
class CheckBecomeUser(StandardBase):
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,13 +1,14 @@
import re
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
from ansiblelater.utils import count_spaces
class CheckBracesSpaces(RuleBase):
rid = "ANS104"
class CheckBracesSpaces(StandardBase):
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,13 +18,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckChangedInWhen(RuleBase):
rid = "ANS126"
class CheckChangedInWhen(StandardBase):
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,13 +1,14 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckCommandHasChanges(RuleBase):
rid = "ANS111"
class CheckCommandHasChanges(StandardBase):
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,13 +20,14 @@
import os
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckCommandInsteadOfArgument(RuleBase):
rid = "ANS117"
class CheckCommandInsteadOfArgument(StandardBase):
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,12 +1,13 @@
import os
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckCommandInsteadOfModule(RuleBase):
rid = "ANS108"
class CheckCommandInsteadOfModule(StandardBase):
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,13 +1,14 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckCompareToEmptyString(RuleBase):
rid = "ANS112"
class CheckCompareToEmptyString(StandardBase):
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,13 +1,14 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckCompareToLiteralBool(RuleBase):
rid = "ANS113"
class CheckCompareToLiteralBool(StandardBase):
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,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckDeprecated(RuleBase):
rid = "ANS999"
class CheckDeprecated(StandardBase):
sid = "ANSIBLE9999"
description = "Deprecated features should not be used"
helptext = "`{old}` is deprecated and should not be used anymore. Use `{new}` instead."
helptext = "'{old}' is deprecated and should not be used anymore. Use '{new}' instead."
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -20,17 +20,18 @@
import os
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
from ansiblelater.utils import has_glob, has_jinja
class CheckDeprecatedBareVars(RuleBase):
rid = "ANS127"
class CheckDeprecatedBareVars(StandardBase):
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

@ -1,132 +0,0 @@
# Original code written by the authors of ansible-lint
from ansiblelater.rule import RuleBase
from ansiblelater.utils import load_plugin
class CheckFQCNBuiltin(RuleBase):
rid = "ANS128"
helptext = "use FQCN `{module_alias}` for module action `{module}`"
description = "Module actions should use full qualified collection names"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
module_aliases = {"block/always/rescue": "block/always/rescue"}
def check(self, candidate, settings):
tasks, errors = self.get_normalized_tasks(candidate, settings)
_builtins = [
"add_host",
"apt",
"apt_key",
"apt_repository",
"assemble",
"assert",
"async_status",
"blockinfile",
"command",
"copy",
"cron",
"debconf",
"debug",
"dnf",
"dpkg_selections",
"expect",
"fail",
"fetch",
"file",
"find",
"gather_facts",
"get_url",
"getent",
"git",
"group",
"group_by",
"hostname",
"import_playbook",
"import_role",
"import_tasks",
"include",
"include_role",
"include_tasks",
"include_vars",
"iptables",
"known_hosts",
"lineinfile",
"meta",
"package",
"package_facts",
"pause",
"ping",
"pip",
"raw",
"reboot",
"replace",
"rpm_key",
"script",
"service",
"service_facts",
"set_fact",
"set_stats",
"setup",
"shell",
"slurp",
"stat",
"subversion",
"systemd",
"sysvinit",
"tempfile",
"template",
"unarchive",
"uri",
"user",
"wait_for",
"wait_for_connection",
"yum",
"yum_repository",
]
if errors:
return self.Result(candidate.path, errors)
for task in tasks:
module = task["action"]["__ansible_module_original__"]
if module not in self.module_aliases:
loaded_module = load_plugin(module)
target = loaded_module.resolved_fqcn
self.module_aliases[module] = target
if target is None:
self.module_aliases[module] = module
continue
if target not in self.module_aliases:
self.module_aliases[target] = target
if module != self.module_aliases[module]:
module_alias = self.module_aliases[module]
if module_alias.startswith("ansible.builtin"):
legacy_module = module_alias.replace(
"ansible.builtin.",
"ansible.legacy.",
1,
)
if module != legacy_module:
helptext = self.helptext.format(module_alias=module_alias, module=module)
if module == "ansible.builtin.include":
helptext = (
"`ansible.builtin.include_task` or `ansible.builtin.import_tasks` "
f"should be used instead of deprecated `{module}`",
)
errors.append(self.Error(task["__line__"], helptext))
else:
if module.count(".") < 2:
errors.append(
self.Error(
task["__line__"],
self.helptext.format(module_alias=module_alias, module=module),
)
)
return self.Result(candidate.path, errors)

View File

@ -19,16 +19,17 @@
# THE SOFTWARE.
import re
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckFilePermissionMissing(RuleBase):
rid = "ANS118"
class CheckFilePermissionMissing(StandardBase):
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,13 +18,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckFilePermissionOctal(RuleBase):
rid = "ANS119"
description = "Numeric file permissions without a leading zero can behave unexpectedly"
helptext = '`mode: {mode}` should be strings with a leading zero `mode: "0{mode}"`'
class CheckFilePermissionOctal(StandardBase):
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):
@ -47,9 +48,7 @@ class CheckFilePermissionOctal(RuleBase):
mode = task["action"].get("mode", None)
if isinstance(mode, int) and self._is_invalid_permission(mode):
errors.append(
self.Error(task["__line__"], self.helptext.format(mode=mode))
)
errors.append(self.Error(task["__line__"], self.helptext))
return self.Result(candidate.path, errors)

View File

@ -1,12 +1,13 @@
import re
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckFilterSeparation(RuleBase):
rid = "ANS116"
class CheckFilterSeparation(StandardBase):
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,13 +18,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckGitHasVersion(RuleBase):
rid = "ANS120"
class CheckGitHasVersion(StandardBase):
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,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckInstallUseLatest(RuleBase):
rid = "ANS109"
class CheckInstallUseLatest(StandardBase):
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,89 +0,0 @@
# Original code written by the authors of ansible-lint
import functools
from ansiblelater.rule import RuleBase
SORTER_TASKS = (
"name",
# "__module__",
# "action",
# "args",
None, # <-- None include all modules that not using action and *
# "when",
# "notify",
# "tags",
"block",
"rescue",
"always",
)
class CheckKeyOrder(RuleBase):
rid = "ANS129"
description = "Check for recommended key order"
helptext = "{type} key order can be improved to `{sorted_keys}`"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):
errors = []
tasks, err = self.get_normalized_tasks(candidate, settings)
if err:
return self.Result(candidate.path, err)
for task in tasks:
is_sorted, keys = self._sort_keys(task.get("__raw_task__"))
if not is_sorted:
errors.append(
self.Error(
task["__line__"],
self.helptext.format(type="task", sorted_keys=", ".join(keys)),
)
)
if candidate.kind == "playbook":
tasks, err = self.get_tasks(candidate, settings)
if err:
return self.Result(candidate.path, err)
for task in tasks:
is_sorted, keys = self._sort_keys(task)
if not is_sorted:
errors.append(
self.Error(
task["__line__"],
self.helptext.format(type="play", sorted_keys=", ".join(keys)),
)
)
return self.Result(candidate.path, errors)
@staticmethod
def _sort_keys(task):
if not task:
return True, []
keys = [str(key) for key in task if not key.startswith("_")]
sorted_keys = sorted(keys, key=functools.cmp_to_key(_task_property_sorter))
return (keys == sorted_keys), sorted_keys
def _task_property_sorter(property1, property2):
"""Sort task properties based on SORTER."""
v_1 = _get_property_sort_index(property1)
v_2 = _get_property_sort_index(property2)
return (v_1 > v_2) - (v_1 < v_2)
def _get_property_sort_index(name):
"""Return the index of the property in the sorter."""
a_index = -1
for i, v in enumerate(SORTER_TASKS):
if v == name:
return i
if v is None:
a_index = i
return a_index

View File

@ -1,12 +1,13 @@
import re
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckLiteralBoolFormat(RuleBase):
rid = "ANS114"
class CheckLiteralBoolFormat(StandardBase):
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,12 +1,13 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckLocalAction(RuleBase):
rid = "ANS124"
class CheckLocalAction(StandardBase):
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,13 +1,14 @@
# Copyright (c) 2018, Ansible Project
from nested_lookup import nested_lookup
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckMetaChangeFromDefault(RuleBase):
rid = "ANS121"
class CheckMetaChangeFromDefault(StandardBase):
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,12 +1,13 @@
from nested_lookup import nested_lookup
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckMetaMain(RuleBase):
rid = "ANS102"
class CheckMetaMain(StandardBase):
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,12 +1,13 @@
from collections import defaultdict
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckNameFormat(RuleBase):
rid = "ANS107"
class CheckNameFormat(StandardBase):
sid = "ANSIBLE0007"
description = "Name of tasks and handlers must be formatted"
helptext = "name `{name}` should start with uppercase"
helptext = "name '{name}' should start with uppercase"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

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

View File

@ -1,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckNativeYaml(RuleBase):
rid = "YML108"
class CheckNativeYaml(StandardBase):
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,16 +21,17 @@
# THE SOFTWARE.
import re
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckNestedJinja(RuleBase):
rid = "ANS123"
class CheckNestedJinja(StandardBase):
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,12 +1,13 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckRelativeRolePaths(RuleBase):
rid = "ANS125"
class CheckRelativeRolePaths(StandardBase):
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,12 +1,13 @@
from ansible.parsing.yaml.objects import AnsibleMapping
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckScmInSrc(RuleBase):
rid = "ANS105"
class CheckScmInSrc(StandardBase):
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,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckShellInsteadCommand(RuleBase):
rid = "ANS110"
class CheckShellInsteadCommand(StandardBase):
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,13 +1,14 @@
import re
from collections import defaultdict
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckTaskSeparation(RuleBase):
rid = "ANS101"
class CheckTaskSeparation(StandardBase):
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,12 +1,13 @@
from collections import defaultdict
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckUniqueNamedTask(RuleBase):
rid = "ANS103"
class CheckUniqueNamedTask(StandardBase):
sid = "ANSIBLE0003"
description = "Tasks and handlers must be uniquely named within a single file"
helptext = "name `{name}` appears multiple times"
helptext = "name '{name}' appears multiple times"
version = "0.1"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -0,0 +1,16 @@
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,13 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckWhenFormat(RuleBase):
rid = "ANS122"
class CheckWhenFormat(StandardBase):
sid = "ANSIBLE0022"
description = "Don't use Jinja2 in when"
helptext = (
"`when` is a raw Jinja2 expression, redundant `{{ }}` should be removed from variable(s)"
"`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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlColons(RuleBase):
rid = "YML105"
class CheckYamlColons(StandardBase):
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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlDocumentEnd(RuleBase):
rid = "YML109"
description = "YAML document end marker should match configuration"
class CheckYamlDocumentEnd(StandardBase):
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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlDocumentStart(RuleBase):
rid = "YML104"
description = "YAML document start marker should match configuration"
class CheckYamlDocumentStart(StandardBase):
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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlEmptyLines(RuleBase):
rid = "YML101"
class CheckYamlEmptyLines(StandardBase):
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,12 +1,13 @@
import os
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlFile(RuleBase):
rid = "YML106"
class CheckYamlFile(StandardBase):
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,10 +1,11 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlHasContent(RuleBase):
rid = "YML107"
class CheckYamlHasContent(StandardBase):
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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlHyphens(RuleBase):
rid = "YML103"
class CheckYamlHyphens(StandardBase):
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,9 +1,10 @@
from ansiblelater.rule import RuleBase
from ansiblelater.standard import StandardBase
class CheckYamlIndent(RuleBase):
rid = "YML102"
class CheckYamlIndent(StandardBase):
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

@ -1,13 +0,0 @@
from ansiblelater.rule import RuleBase
class CheckYamlOctalValues(RuleBase):
rid = "YML110"
description = "YAML implicit/explicit octal value should match configuration"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings):
options = f"rules: {{octal-values: {settings['yamllint']['octal-values']}}}"
errors = self.run_yamllint(candidate, options)
return self.Result(candidate.path, errors)

View File

@ -1,6 +1,5 @@
"""Global settings object definition."""
import importlib.resources
import os
import anyconfig
@ -8,6 +7,7 @@ import jsonschema.exceptions
import pathspec
from appdirs import AppDirs
from jsonschema._utils import format_as_index
from pkg_resources import resource_filename
from ansiblelater import utils
@ -104,13 +104,13 @@ class Settings:
if f not in defaults["ansible"]["custom_modules"]:
defaults["ansible"]["custom_modules"].append(f)
if defaults["rules"]["builtin"]:
ref = importlib.resources.files("ansiblelater") / "rules"
with importlib.resources.as_file(ref) as path:
defaults["rules"]["dir"].append(path)
if defaults["rules"]["buildin"]:
defaults["rules"]["standards"].append(
os.path.join(resource_filename("ansiblelater", "rules"))
)
defaults["rules"]["dir"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["dir"]
defaults["rules"]["standards"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["standards"]
]
return defaults
@ -118,16 +118,17 @@ class Settings:
def _get_defaults(self):
defaults = {
"rules": {
"builtin": True,
"dir": [],
"include_filter": [],
"buildin": True,
"standards": [],
"filter": [],
"exclude_filter": [],
"warning_filter": [
"ANS128",
"ANS999",
"ANSIBLE9999",
"ANSIBLE9998",
],
"ignore_dotfiles": True,
"exclude_files": [],
"version": "",
},
"logging": {
"level": "WARNING",
@ -144,7 +145,7 @@ class Settings:
"exclude": [
"meta",
"debug",
"block/always/rescue",
"block",
"include_role",
"import_role",
"include_tasks",
@ -174,16 +175,12 @@ class Settings:
"present": True,
},
"document-end": {
"present": False,
"present": True,
},
"colons": {
"max-spaces-before": 0,
"max-spaces-after": 1,
},
"octal-values": {
"forbid-implicit-octal": True,
"forbid-explicit-octal": True,
},
},
}

View File

@ -1,4 +1,4 @@
"""Rule definition."""
"""Standard definition."""
import copy
import importlib
@ -27,26 +27,27 @@ from ansiblelater.utils.yamlhelper import (
)
class RuleMeta(type):
class StandardMeta(type):
def __call__(cls, *args):
mcls = type.__call__(cls, *args)
mcls.rid = cls.rid
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 RuleExtendedMeta(RuleMeta, ABCMeta):
class StandardExtendedMeta(StandardMeta, ABCMeta):
pass
class RuleBase(metaclass=RuleExtendedMeta):
class StandardBase(metaclass=StandardExtendedMeta):
SHELL_PIPE_CHARS = "&|<>;$\n*[]{}?"
@property
@abstractmethod
def rid(self):
def sid(self):
pass
@abstractmethod
@ -54,7 +55,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
pass
def __repr__(self):
return f"Rule: {self.description} (types: {self.types})"
return f"Standard: {self.description} (version: {self.version}, types: {self.types})"
@staticmethod
def get_tasks(candidate, settings): # noqa
@ -68,11 +69,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -92,11 +93,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return tasks, errors
@ -114,11 +115,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -149,21 +150,20 @@ class RuleBase(metaclass=RuleExtendedMeta):
# No need to normalize_task if we are skipping it.
continue
normalized_task = normalize_task(
task, candidate.path, settings["ansible"]["custom_modules"]
normalized.append(
normalize_task(
task, candidate.path, settings["ansible"]["custom_modules"]
)
)
normalized_task["__raw_task__"] = task
normalized.append(normalized_task)
except LaterError as ex:
e = ex.original
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -184,11 +184,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -210,7 +210,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
content = yaml.safe_load(f)
except yaml.YAMLError as e:
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
@ -224,14 +224,14 @@ class RuleBase(metaclass=RuleExtendedMeta):
try:
with open(candidate.path, encoding="utf-8") as f:
for problem in linter.run(f, YamlLintConfig(options)):
errors.append(RuleBase.Error(problem.line, problem.desc))
errors.append(StandardBase.Error(problem.line, problem.desc))
except yaml.YAMLError as e:
errors.append(
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except (TypeError, ValueError) as e:
errors.append(RuleBase.Error(None, f"yamllint error: {e}"))
errors.append(StandardBase.Error(None, f"yamllint error: {e}"))
candidate.faulty = True
return errors
@ -302,7 +302,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
return "\n".join([f"{self.candidate}:{error}" for error in self.errors])
class RulesLoader:
class StandardLoader:
def __init__(self, source):
self.rules = []
@ -331,20 +331,23 @@ class RulesLoader:
def _is_plugin(self, obj):
return (
inspect.isclass(obj) and issubclass(obj, RuleBase) and obj is not RuleBase and not None
inspect.isclass(obj)
and issubclass(obj, StandardBase)
and obj is not StandardBase
and not None
)
def validate(self):
normalize_rule = list(toolz.remove(lambda x: x.rid == "", self.rules))
unique_rule = len(list(toolz.unique(normalize_rule, key=lambda x: x.rid)))
all_rules = len(normalize_rule)
if all_rules != unique_rule:
normalized_std = list(toolz.remove(lambda x: x.sid == "", self.rules))
unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.sid)))
all_std = len(normalized_std)
if all_std != unique_std:
sysexit_with_message(
"Found duplicate tags in rules definition. Please use unique tags only."
"Detect duplicate ID's in standards definition. Please use unique ID's only."
)
class SingleRules(RulesLoader, metaclass=Singleton):
class SingleStandards(StandardLoader, metaclass=Singleton):
"""Singleton config class."""
pass

View File

@ -4,10 +4,9 @@ import contextlib
import re
import sys
from contextlib import suppress
from functools import lru_cache
import yaml
from ansible.plugins.loader import module_loader
from packaging.version import Version
from ansiblelater import logger
@ -36,6 +35,12 @@ 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
@ -79,7 +84,9 @@ def open_file(filename, mode="r"):
def add_dict_branch(tree, vector, value):
key = vector[0]
tree[key] = (
value if len(vector) == 1 else add_dict_branch(tree.get(key, {}), vector[1:], value)
value
if len(vector) == 1
else add_dict_branch(tree[key] if key in tree else {}, vector[1:], value)
)
return tree
@ -114,21 +121,3 @@ class Singleton(type):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
@lru_cache
def load_plugin(name):
"""Return loaded ansible plugin/module."""
loaded_module = module_loader.find_plugin_with_context(
name,
ignore_deprecated=True,
check_aliases=True,
)
if not loaded_module.resolved and name.startswith("ansible.builtin."):
# fallback to core behavior of using legacy
loaded_module = module_loader.find_plugin_with_context(
name.replace("ansible.builtin.", "ansible.legacy."),
ignore_deprecated=True,
check_aliases=True,
)
return loaded_module

View File

@ -65,11 +65,11 @@ def ansible_template(basedir, varname, templatevars, **kwargs):
try:
from ansible.plugins import module_loader
except ImportError:
from ansible.plugins.loader import init_plugin_loader, module_loader
init_plugin_loader()
except ImportError:
from ansible.plugins.loader import module_loader
LINE_NUMBER_KEY = "__line__"
FILENAME_KEY = "__file__"
@ -369,63 +369,12 @@ def _kv_to_dict(v):
def normalize_task(task, filename, custom_modules=None):
"""Ensure tasks have an action key and strings are converted to python objects."""
def _normalize(task, custom_modules):
if custom_modules is None:
custom_modules = []
if custom_modules is None:
custom_modules = []
normalized = {}
ansible_parsed_keys = ("action", "local_action", "args", "delegate_to")
if is_nested_task(task):
_extract_ansible_parsed_keys_from_task(normalized, task, ansible_parsed_keys)
# Add dummy action for block/always/rescue statements
normalized["action"] = {
"__ansible_module__": "block/always/rescue",
"__ansible_module_original__": "block/always/rescue",
"__ansible_arguments__": "block/always/rescue",
}
return normalized
builtin = list(ansible.parsing.mod_args.BUILTIN_TASKS)
builtin = list(set(builtin + custom_modules))
ansible.parsing.mod_args.BUILTIN_TASKS = frozenset(builtin)
mod_arg_parser = ModuleArgsParser(task)
try:
action, arguments, normalized["delegate_to"] = mod_arg_parser.parse()
except AnsibleParserError as e:
raise LaterAnsibleError(e) from e
# denormalize shell -> command conversion
if "_uses_shell" in arguments:
action = "shell"
del arguments["_uses_shell"]
for k, v in list(task.items()):
if k in ansible_parsed_keys or k == action:
# we don"t want to re-assign these values, which were
# determined by the ModuleArgsParser() above
continue
normalized[k] = v
# convert builtin fqn calls to short forms because most rules know only
# about short calls
normalized["action"] = {
"__ansible_module__": action.removeprefix("ansible.builtin."),
"__ansible_module_original__": action,
}
if "_raw_params" in arguments:
normalized["action"]["__ansible_arguments__"] = (
arguments["_raw_params"].strip().split()
)
del arguments["_raw_params"]
else:
normalized["action"]["__ansible_arguments__"] = []
normalized["action"].update(arguments)
return normalized
ansible_action_type = task.get("__ansible_action_type__", "task")
if "__ansible_action_type__" in task:
del task["__ansible_action_type__"]
# temp. extract metadata
ansible_meta = {}
@ -437,11 +386,39 @@ def normalize_task(task, filename, custom_modules=None):
ansible_meta[key] = task.pop(key, default)
ansible_action_type = task.get("__ansible_action_type__", "task")
if "__ansible_action_type__" in task:
del task["__ansible_action_type__"]
normalized = {}
normalized = _normalize(task, custom_modules)
builtin = list(ansible.parsing.mod_args.BUILTIN_TASKS)
builtin = list(set(builtin + custom_modules))
ansible.parsing.mod_args.BUILTIN_TASKS = frozenset(builtin)
mod_arg_parser = ModuleArgsParser(task)
try:
action, arguments, normalized["delegate_to"] = mod_arg_parser.parse()
except AnsibleParserError as e:
raise LaterAnsibleError(e) from e
# denormalize shell -> command conversion
if "_uses_shell" in arguments:
action = "shell"
del arguments["_uses_shell"]
for k, v in list(task.items()):
if k in ("action", "local_action", "args", "delegate_to") or k == action:
# we don"t want to re-assign these values, which were
# determined by the ModuleArgsParser() above
continue
normalized[k] = v
normalized["action"] = {"__ansible_module__": action}
if "_raw_params" in arguments:
normalized["action"]["__ansible_arguments__"] = arguments["_raw_params"].strip().split()
del arguments["_raw_params"]
else:
normalized["action"]["__ansible_arguments__"] = []
normalized["action"].update(arguments)
normalized[FILENAME_KEY] = filename
normalized["__ansible_action_type__"] = ansible_action_type
@ -454,17 +431,22 @@ def normalize_task(task, filename, custom_modules=None):
return normalized
def action_tasks(yaml, candidate):
def action_tasks(yaml, file):
tasks = []
if candidate.filemeta in ["tasks", "handlers"]:
tasks = add_action_type(yaml, candidate.filemeta)
if file["filetype"] in ["tasks", "handlers"]:
tasks = add_action_type(yaml, file["filetype"])
else:
tasks.extend(extract_from_list(yaml, ["tasks", "handlers", "pre_tasks", "post_tasks"]))
# Add sub-elements of block/rescue/always to tasks list
tasks.extend(extract_from_list(tasks, ["block", "rescue", "always"]))
# Remove block/rescue/always elements from tasks list
block_rescue_always = ("block", "rescue", "always")
tasks[:] = [task for task in tasks if all(k not in task for k in block_rescue_always)]
return tasks
allowed = ["include", "include_tasks", "import_playbook", "import_tasks"]
return [task for task in tasks if set(allowed).isdisjoint(task.keys())]
def task_to_str(task):
@ -493,10 +475,7 @@ def extract_from_list(blocks, candidates):
meta_data = dict(block)
for key in delete_meta_keys:
meta_data.pop(key, None)
actions = add_action_type(block[candidate], candidate, meta_data)
results.extend(actions)
results.extend(add_action_type(block[candidate], candidate, meta_data))
elif block[candidate] is not None:
raise RuntimeError(
f"Key '{candidate}' defined, but bad value: '{block[candidate]!s}'"
@ -585,26 +564,6 @@ def normalized_yaml(file, options):
return lines
def is_nested_task(task):
"""Check if task includes block/always/rescue."""
# Cannot really trust the input
if isinstance(task, str):
return False
return any(task.get(key) for key in ["block", "rescue", "always"])
def _extract_ansible_parsed_keys_from_task(result, task, keys):
"""Return a dict with existing key in task."""
for k, v in list(task.items()):
if k in keys:
# we don't want to re-assign these values, which were
# determined by the ModuleArgsParser() above
continue
result[k] = v
return result
class UnsafeTag:
"""Handle custom yaml unsafe tag."""

View File

@ -1,17 +1,18 @@
---
title: Write a rule
title: Minimal standard checks
---
A typical rule check will look like:
A typical standards check will look like:
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< highlight Python "linenos=table" >}}
class CheckBecomeUser(RuleBase):
class CheckBecomeUser(StandardBase):
rid = "ANS115"
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,27 +8,28 @@ 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] [-r DIR] [-B] [-i TAGS] [-x TAGS] [-v] [-q] [-V] [rules.files ...]
usage: ansible-later [-h] [-c CONFIG_FILE] [-r RULES.STANDARDS]
[-s RULES.FILTER] [-v] [-q] [--version]
[rules.files [rules.files ...]]
Validate Ansible files against best practice guideline
positional arguments:
rules.files
options:
optional arguments:
-h, --help show this help message and exit
-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
-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
-v increase log level
-q decrease log level
-V, --version show program's version number and exit
--version show program's version number and exit
{{< /highlight >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->

View File

@ -16,32 +16,32 @@ ansible:
# directory will be auto-detected and don't need to be added to this list.
custom_modules: []
# Settings for variable formatting rule (ANS104)
# Settings for variable formatting rule (ANSIBLE0004)
double-braces:
max-spaces-inside: 1
min-spaces-inside: 1
# List of allowed literal bools (ANS114)
# List of allowed literal bools (ANSIBLE0014)
literal-bools:
- "True"
- "False"
- "yes"
- "no"
# List of modules that don't need to be named (ANS106).
# List of modules that don't need to be named (ANSIBLE0006).
# You must specify each individual module name, globs or wildcards do not work!
named-task:
exclude:
- "meta"
- "debug"
- "block/always/rescue"
- "block"
- "include_role"
- "include_tasks"
- "include_vars"
- "import_role"
- "import_tasks"
# List of modules that are allowed to use the key=value format instead of the native YAML format (YML108).
# List of modules that are allowed to use the key=value format instead of the native YAML format (LINT0008).
# You must specify each individual module name, globs or wildcards do not work!
native-yaml:
exclude: []
@ -58,8 +58,8 @@ logging:
# Global settings for all defined rules
rules:
# Disable built-in rules if required
builtin: True
# Disable build-in rules if required
buildin: True
# List of files to exclude
exclude_files: []
@ -75,17 +75,22 @@ rules:
exclude_filter: []
# List of rule ID's that should be displayed as a warning instead of an error. By default,
# no rules are marked as warnings. This list allows to degrade errors to warnings for each rule.
# 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.
warning_filter:
- "ANS128"
- "ANS999"
- "ANSIBLE9999"
- "ANSIBLE9998"
# All dotfiles (including hidden folders) are excluded by default.
# You can disable this setting and handle dotfiles by yourself with `exclude_files`.
ignore_dotfiles: True
# List of directories to load rules from (defaults to built-in)
dir: []
# 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:
# Block to control included yamllint rules.
# See https://yamllint.readthedocs.io/en/stable/rules.html
@ -95,8 +100,6 @@ yamllint:
max-spaces-before: 0
document-start:
present: True
document-end:
present: True
empty-lines:
max: 1
max-end: 1

View File

@ -2,47 +2,45 @@
title: Included rules
---
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.
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.
| Rule | ID | Description | Parameter |
| ----------------------------- | ------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------- |
| CheckYamlEmptyLines | YML101 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} |
| CheckYamlIndent | YML102 | YAML should be correctly indented. | {spaces: 2, check-multi-line-strings: false, indent-sequences: true} |
| CheckYamlHyphens | YML103 | YAML should use consistent number of spaces after hyphens (-). | {max-spaces-after: 1} |
| CheckYamlDocumentStart | YML104 | YAML should contain document start marker. | {document-start: {present: true}} |
| CheckYamlColons | YML105 | YAML should use consistent number of spaces around colons. | {colons: {max-spaces-before: 0, max-spaces-after: 1}} |
| CheckYamlFile | YML106 | Roles file should be in YAML format. | |
| CheckYamlHasContent | YML107 | Files should contain useful content. | |
| CheckNativeYaml | YML108 | Use YAML format for tasks and handlers rather than key=value. | {native-yaml: {exclude: []}} |
| CheckYamlDocumentEnd | YML109 | YAML should contain document end marker. | {document-end: {present: true}} |
| CheckYamlOctalValues | YML110 | YAML should not use forbidden implicit or explicit octal value. | {octal-values: {forbid-implicit-octal: true, forbid-explicit-octal: true}} |
| CheckTaskSeparation | ANS101 | Single tasks should be separated by an empty line. | |
| CheckMetaMain | ANS102 | Meta file should contain a basic subset of parameters. | author, description, min_ansible_version, platforms, dependencies |
| CheckUniqueNamedTask | ANS103 | Tasks and handlers must be uniquely named within a file. | |
| CheckBraces | ANS104 | YAML should use consistent number of spaces around variables. | {double-braces: max-spaces-inside: 1, min-spaces-inside: 1} |
| CheckScmInSrc | ANS105 | Use SCM key rather than `src: scm+url` in requirements file. | |
| CheckNamedTask | ANS106 | Tasks and handlers must be named. | {named-task: {exclude: [meta, debug, block, include\_\*, import\_\*]}} |
| CheckNameFormat | ANS107 | Name of tasks and handlers must be formatted. | formats: first letter capital |
| CheckCommandInsteadofModule | ANS108 | Commands should not be used in place of modules. | |
| CheckInstallUseLatest | ANS109 | Package managers should not install with state=latest. | |
| CheckShellInsteadCommand | ANS110 | Use Shell only when piping, redirecting or chaining commands. | |
| CheckCommandHasChanges | ANS111 | Commands should be idempotent and only used with some checks. | |
| CheckCompareToEmptyString | ANS112 | Don't compare to "" - use `when: var` or `when: not var`. | |
| CheckCompareToLiteralBool | ANS113 | Don't compare to True/False - use `when: var` or `when: not var`. | |
| CheckLiteralBoolFormat | ANS114 | Literal bools should be consistent. | {literal-bools: [True, False, yes, no]} |
| CheckBecomeUser | ANS115 | Become should be combined with become_user. | |
| CheckFilterSeparation | ANS116 | Jinja2 filters should be separated with spaces. | |
| CheckCommandInsteadOfArgument | ANS117 | Commands should not be used in place of module arguments. | |
| CheckFilePermissionMissing | ANS118 | File permissions unset or incorrect. | |
| CheckFilePermissionOctal | ANS119 | Octal file permissions must contain leading zero or be a string. | |
| CheckGitHasVersion | ANS120 | Git checkouts should use explicit version. | |
| CheckMetaChangeFromDefault | ANS121 | Roles meta/main.yml default values should be changed. | |
| CheckWhenFormat | ANS122 | Don't use Jinja2 in `when`. | |
| CheckNestedJinja | ANS123 | Don't use nested Jinja2 pattern. | |
| CheckLocalAction | ANS124 | Don't use local_action. | |
| CheckRelativeRolePaths | ANS125 | Don't use a relative path in a role. | |
| CheckChangedInWhen | ANS126 | Use handlers instead of `when: changed`. | |
| CheckChangedInWhen | ANS127 | Deprecated bare variables in loops must not be used. | |
| CheckFQCNBuiltin | ANS128 | Module actions should use full qualified collection names. | |
| CheckFQCNBuiltin | ANS129 | Check optimized playbook/tasks key order. | |
| CheckDeprecated | ANS999 | Deprecated features of `ansible-later` should not be used. | |
| Rule | ID | Description | Parameter |
| ----------------------------- | ----------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
| CheckYamlEmptyLines | LINT0001 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} |
| CheckYamlIndent | LINT0002 | YAML should be correctly indented. | {spaces: 2, check-multi-line-strings: false, indent-sequences: true} |
| CheckYamlHyphens | LINT0003 | YAML should use consistent number of spaces after hyphens (-). | {max-spaces-after: 1} |
| CheckYamlDocumentStart | LINT0004 | YAML should contain document start marker. | {document-start: {present: true}} |
| CheckYamlColons | LINT0005 | YAML should use consistent number of spaces around colons. | {colons: {max-spaces-before: 0, max-spaces-after: 1}} |
| CheckYamlFile | LINT0006 | Roles file should be in YAML format. | |
| CheckYamlHasContent | LINT0007 | Files should contain useful content. | |
| CheckNativeYaml | LINT0008 | Use YAML format for tasks and handlers rather than key=value. | {native-yaml: {exclude: []}} |
| CheckYamlDocumentEnd | LINT0009 | YAML should contain document end marker. | {document-end: {present: true}} |
| CheckTaskSeparation | ANSIBLE0001 | Single tasks should be separated by an empty line. | |
| CheckMetaMain | ANSIBLE0002 | Meta file should contain a basic subset of parameters. | author, description, min_ansible_version, platforms, dependencies |
| CheckUniqueNamedTask | ANSIBLE0003 | Tasks and handlers must be uniquely named within a file. | |
| CheckBraces | ANSIBLE0004 | YAML should use consistent number of spaces around variables. | {double-braces: max-spaces-inside: 1, min-spaces-inside: 1} |
| CheckScmInSrc | ANSIBLE0005 | Use SCM key rather than `src: scm+url` in requirements file. | |
| CheckNamedTask | ANSIBLE0006 | Tasks and handlers must be named. | {named-task: {exclude: [meta, debug, block, include\_\*, import\_\*]}} |
| CheckNameFormat | ANSIBLE0007 | Name of tasks and handlers must be formatted. | formats: first letter capital |
| CheckCommandInsteadofModule | ANSIBLE0008 | Commands should not be used in place of modules. | |
| CheckInstallUseLatest | ANSIBLE0009 | Package managers should not install with state=latest. | |
| CheckShellInsteadCommand | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands. | |
| CheckCommandHasChanges | ANSIBLE0011 | Commands should be idempotent and only used with some checks. | |
| CheckCompareToEmptyString | ANSIBLE0012 | Don't compare to "" - use `when: var` or `when: not var`. | |
| CheckCompareToLiteralBool | ANSIBLE0013 | Don't compare to True/False - use `when: var` or `when: not var`. | |
| CheckLiteralBoolFormat | ANSIBLE0014 | Literal bools should be consistent. | {literal-bools: [True, False, yes, no]} |
| CheckBecomeUser | ANSIBLE0015 | Become should be combined with become_user. | |
| CheckFilterSeparation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | |
| CheckCommandInsteadOfArgument | ANSIBLE0017 | Commands should not be used in place of module arguments. | |
| CheckFilePermissionMissing | ANSIBLE0018 | File permissions unset or incorrect. | |
| CheckFilePermissionOctal | ANSIBLE0019 | Octal file permissions must contain leading zero or be a string. | |
| CheckGitHasVersion | ANSIBLE0020 | Git checkouts should use explicit version. | |
| CheckMetaChangeFromDefault | ANSIBLE0021 | Roles meta/main.yml default values should be changed. | |
| CheckWhenFormat | ANSIBLE0022 | Don't use Jinja2 in `when`. | |
| CheckNestedJinja | ANSIBLE0023 | Don't use nested Jinja2 pattern. | |
| CheckLocalAction | ANSIBLE0024 | Don't use local_action. | |
| 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: Rules
ref: "/build_rules/rule"
- name: Standards checks
ref: "/build_rules/standards_check"

964
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,30 +20,34 @@ classifiers = [
description = "Reviews ansible playbooks, roles and inventories and suggests improvements."
documentation = "https://ansible-later.geekdocs.de/"
homepage = "https://ansible-later.geekdocs.de/"
include = ["LICENSE"]
include = [
"LICENSE",
]
keywords = ["ansible", "code", "review"]
license = "MIT"
name = "ansible-later"
packages = [{ include = "ansiblelater" }]
packages = [
{include = "ansiblelater"},
]
readme = "README.md"
repository = "https://github.com/thegeeklab/ansible-later/"
version = "0.0.0"
[tool.poetry.dependencies]
PyYAML = "6.0.2"
ansible-core = { version = "2.14.17", optional = true }
ansible = { version = "7.7.0", optional = true }
anyconfig = "0.14.0"
PyYAML = "6.0.1"
ansible = {version = "8.7.0", optional = true}
ansible-core = {version = "2.15.8", optional = true}
anyconfig = "0.13.0"
appdirs = "1.4.4"
colorama = "0.4.6"
jsonschema = "4.23.0"
jsonschema = "4.20.0"
nested-lookup = "0.2.25"
pathspec = "0.12.1"
python = "^3.9.0"
python-json-logger = "2.0.7"
toolz = "1.0.0"
toolz = "0.12.0"
unidiff = "0.7.5"
yamllint = "1.35.1"
yamllint = "1.33.0"
[tool.poetry.extras]
ansible = ["ansible"]
@ -53,10 +57,10 @@ ansible-core = ["ansible-core"]
ansible-later = "ansiblelater.__main__:main"
[tool.poetry.group.dev.dependencies]
ruff = "0.7.2"
pytest = "8.3.3"
pytest-mock = "3.14.0"
pytest-cov = "6.0.0"
ruff = "0.1.11"
pytest = "7.4.4"
pytest-mock = "3.12.0"
pytest-cov = "4.1.0"
toml = "0.10.2"
[tool.poetry-dynamic-versioning]
@ -81,22 +85,21 @@ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
[tool.ruff]
exclude = [
".git",
"__pycache__",
"build",
"dist",
"test",
"*.pyc",
"*.egg-info",
".cache",
".eggs",
"env*",
".git",
"__pycache__",
"build",
"dist",
"test",
"*.pyc",
"*.egg-info",
".cache",
".eggs",
"env*",
]
line-length = 99
indent-width = 4
[tool.ruff.lint]
# Explanation of errors
#
# D100: Missing docstring in public module
@ -109,38 +112,38 @@ indent-width = 4
# D203: One blank line required before class docstring
# D212: Multi-line docstring summary should start at the first line
ignore = [
"D100",
"D101",
"D102",
"D103",
"D105",
"D107",
"D202",
"D203",
"D212",
"UP038",
"RUF012",
"D100",
"D101",
"D102",
"D103",
"D105",
"D107",
"D202",
"D203",
"D212",
"UP038",
"RUF012",
]
select = [
"D",
"E",
"F",
"Q",
"W",
"I",
"S",
"BLE",
"N",
"UP",
"B",
"A",
"C4",
"T20",
"SIM",
"RET",
"ARG",
"ERA",
"RUF",
"D",
"E",
"F",
"Q",
"W",
"I",
"S",
"BLE",
"N",
"UP",
"B",
"A",
"C4",
"T20",
"SIM",
"RET",
"ARG",
"ERA",
"RUF",
]
[tool.ruff.format]