mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-16 18:10:38 +00:00
Compare commits
No commits in common. "main" and "v3.4.3" have entirely different histories.
@ -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].+
|
||||
|
1
.github/settings.yml
vendored
1
.github/settings.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>"
|
||||
|
2
Makefile
2
Makefile
@ -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
|
||||
|
14
README.md
14
README.md
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
16
ansiblelater/rules/CheckVersion.py
Normal file
16
ansiblelater/rules/CheckVersion.py
Normal 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)
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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):
|
@ -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 -->
|
||||
|
@ -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
|
||||
|
@ -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. | |
|
||||
|
@ -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
964
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
111
pyproject.toml
111
pyproject.toml
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user