Compare commits

..

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

76 changed files with 2146 additions and 1816 deletions

23
.chglog/CHANGELOG.tpl.md Executable file
View File

@ -0,0 +1,23 @@
# Changelog
{{ range .Versions -}}
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }})
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ (regexReplaceAll "(.*)/issues/(.*)" (regexReplaceAll "(Co-\\w*-by.*)" .Subject "") "${1}/pull/${2}") | trim }}
{{ end }}
{{- end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}

25
.chglog/config.yml Executable file
View File

@ -0,0 +1,25 @@
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://github.com/thegeeklab/ansible-later
options:
commit_groups:
title_maps:
feat: Features
fix: Bug Fixes
perf: Performance Improvements
refactor: Code Refactoring
chore: Others
test: Testing
ci: CI Pipeline
docs: Documentation
header:
pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
pattern_maps:
- Type
- Scope
- Subject
notes:
keywords:
- BREAKING CHANGE

View File

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

436
.drone.jsonnet Normal file
View File

@ -0,0 +1,436 @@
local PythonVersion(pyversion='3.9') = {
name: 'python' + std.strReplace(pyversion, '.', '') + '-pytest',
image: 'python:' + pyversion,
environment: {
PY_COLORS: 1,
},
commands: [
'pip install poetry poetry-dynamic-versioning -qq',
'poetry install -E ansible-core',
'poetry run pytest --cov-append',
'poetry version',
'poetry run ansible-later --help',
],
depends_on: [
'fetch',
],
};
local PipelineLint = {
kind: 'pipeline',
name: 'lint',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'check-format',
image: 'python:3.11',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry install',
'poetry run yapf -dr ./ansiblelater',
],
},
{
name: 'check-coding',
image: 'python:3.11',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry install -E ansible-core',
'poetry run ruff ./ansiblelater',
],
},
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineTest = {
kind: 'pipeline',
name: 'test',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'fetch',
image: 'python:3.11',
commands: [
'git fetch -tq',
],
},
PythonVersion(pyversion='3.9'),
PythonVersion(pyversion='3.10'),
PythonVersion(pyversion='3.11'),
{
name: 'codecov',
image: 'thegeeklab/codecov',
environment: {
CODECOV_TOKEN: { from_secret: 'codecov_token' },
},
commands: [
'codecov --nonZero',
],
depends_on: [
'python39-pytest',
'python310-pytest',
'python311-pytest',
],
},
],
depends_on: [
'lint',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuildPackage = {
kind: 'pipeline',
name: 'build-package',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'build',
image: 'python:3.11',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry build',
],
},
{
name: 'checksum',
image: 'alpine',
commands: [
'cd dist/ && sha256sum * > ../sha256sum.txt',
],
},
{
name: 'changelog-generate',
image: 'thegeeklab/git-chglog',
commands: [
'git fetch -tq',
'git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased}',
],
},
{
name: 'changelog-format',
image: 'thegeeklab/alpine-tools',
commands: [
'prettier CHANGELOG.md',
'prettier -w CHANGELOG.md',
],
},
{
name: 'publish-github',
image: 'plugins/github-release',
settings: {
overwrite: true,
api_key: { from_secret: 'github_token' },
files: ['dist/*', 'sha256sum.txt'],
title: '${DRONE_TAG}',
note: 'CHANGELOG.md',
},
when: {
ref: ['refs/tags/**'],
},
},
{
name: 'publish-pypi',
image: 'python:3.11',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry publish -n',
],
environment: {
POETRY_HTTP_BASIC_PYPI_USERNAME: { from_secret: 'pypi_username' },
POETRY_HTTP_BASIC_PYPI_PASSWORD: { from_secret: 'pypi_password' },
},
when: {
ref: ['refs/tags/**'],
},
},
],
depends_on: [
'test',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuildContainer = {
kind: 'pipeline',
name: 'build-container',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'build',
image: 'python:3.11',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry build',
],
},
{
name: 'dryrun',
image: 'thegeeklab/drone-docker-buildx:23',
settings: {
dry_run: true,
dockerfile: 'Dockerfile.multiarch',
repo: 'thegeeklab/${DRONE_REPO_NAME}',
platforms: [
'linux/amd64',
'linux/arm64',
],
provenance: false,
},
depends_on: ['build'],
when: {
ref: ['refs/pull/**'],
},
},
{
name: 'publish-dockerhub',
image: 'thegeeklab/drone-docker-buildx:23',
settings: {
auto_tag: true,
dockerfile: 'Dockerfile.multiarch',
repo: 'thegeeklab/${DRONE_REPO_NAME}',
username: { from_secret: 'docker_username' },
password: { from_secret: 'docker_password' },
platforms: [
'linux/amd64',
'linux/arm64',
],
provenance: false,
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
depends_on: ['dryrun'],
},
{
name: 'publish-quay',
image: 'thegeeklab/drone-docker-buildx:23',
settings: {
auto_tag: true,
dockerfile: 'Dockerfile.multiarch',
registry: 'quay.io',
repo: 'quay.io/thegeeklab/${DRONE_REPO_NAME}',
username: { from_secret: 'quay_username' },
password: { from_secret: 'quay_password' },
platforms: [
'linux/amd64',
'linux/arm64',
],
provenance: false,
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
depends_on: ['dryrun'],
},
],
depends_on: [
'test',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineDocs = {
kind: 'pipeline',
name: 'docs',
platform: {
os: 'linux',
arch: 'amd64',
},
concurrency: {
limit: 1,
},
steps: [
{
name: 'assets',
image: 'thegeeklab/alpine-tools',
commands: [
'make doc',
],
},
{
name: 'markdownlint',
image: 'thegeeklab/markdownlint-cli',
commands: [
"markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md'",
],
},
{
name: 'spellcheck',
image: 'thegeeklab/alpine-tools',
commands: [
"spellchecker --files 'docs/content/**/*.md' 'README.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions",
],
environment: {
FORCE_COLOR: true,
NPM_CONFIG_LOGLEVEL: 'error',
},
},
{
name: 'testbuild',
image: 'thegeeklab/hugo:0.114.0',
commands: [
'hugo --panicOnWarning -s docs/ -b http://localhost:8000/',
],
},
{
name: 'link-validation',
image: 'thegeeklab/link-validator',
commands: [
'link-validator --color=always --rate-limit 10',
],
environment: {
LINK_VALIDATOR_BASE_DIR: 'docs/public',
LINK_VALIDATOR_RETRIES: '3',
},
},
{
name: 'build',
image: 'thegeeklab/hugo:0.114.0',
commands: [
'hugo --panicOnWarning -s docs/',
],
},
{
name: 'beautify',
image: 'thegeeklab/alpine-tools',
commands: [
"html-beautify -r -f 'docs/public/**/*.html'",
],
environment: {
FORCE_COLOR: true,
NPM_CONFIG_LOGLEVEL: 'error',
},
},
{
name: 'publish',
image: 'thegeeklab/drone-s3-sync',
settings: {
access_key: { from_secret: 's3_access_key' },
bucket: 'geekdocs',
delete: true,
endpoint: 'https://sp.rknet.org',
path_style: true,
secret_key: { from_secret: 's3_secret_access_key' },
source: 'docs/public/',
strip_prefix: 'docs/public/',
target: '/${DRONE_REPO_NAME}',
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
},
],
depends_on: [
'build-package',
'build-container',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineNotifications = {
kind: 'pipeline',
name: 'notifications',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'pushrm-dockerhub',
image: 'chko/docker-pushrm:1',
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: 'thegeeklab/${DRONE_REPO_NAME}',
},
when: {
status: ['success'],
},
},
{
name: 'pushrm-quay',
image: 'chko/docker-pushrm:1',
environment: {
APIKEY__QUAY_IO: {
from_secret: 'quay_token',
},
PUSHRM_FILE: 'README.md',
PUSHRM_TARGET: 'quay.io/thegeeklab/${DRONE_REPO_NAME}',
},
when: {
status: ['success'],
},
},
{
name: 'matrix',
image: 'thegeeklab/drone-matrix',
settings: {
homeserver: { from_secret: 'matrix_homeserver' },
roomid: { from_secret: 'matrix_roomid' },
template: 'Status: **{{ .Build.Status }}**<br/> Build: [{{ .Repo.Owner }}/{{ .Repo.Name }}]({{ .Build.Link }}){{ if .Build.Branch }} ({{ .Build.Branch }}){{ end }} by {{ .Commit.Author }}<br/> Message: {{ .Commit.Message.Title }}',
username: { from_secret: 'matrix_username' },
password: { from_secret: 'matrix_password' },
},
when: {
status: ['success', 'failure'],
},
},
],
depends_on: [
'docs',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**'],
status: ['success', 'failure'],
},
};
[
PipelineLint,
PipelineTest,
PipelineBuildPackage,
PipelineBuildContainer,
PipelineDocs,
PipelineNotifications,
]

417
.drone.yml Normal file
View File

@ -0,0 +1,417 @@
---
kind: pipeline
name: lint
platform:
os: linux
arch: amd64
steps:
- name: check-format
image: python:3.11
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
- poetry run yapf -dr ./ansiblelater
environment:
PY_COLORS: 1
- name: check-coding
image: python:3.11
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run ruff ./ansiblelater
environment:
PY_COLORS: 1
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
---
kind: pipeline
name: test
platform:
os: linux
arch: amd64
steps:
- name: fetch
image: python:3.11
commands:
- git fetch -tq
- name: python39-pytest
image: python:3.9
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run pytest --cov-append
- poetry version
- poetry run ansible-later --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: python310-pytest
image: python:3.10
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run pytest --cov-append
- poetry version
- poetry run ansible-later --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: python311-pytest
image: python:3.11
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run pytest --cov-append
- poetry version
- poetry run ansible-later --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: codecov
image: thegeeklab/codecov
commands:
- codecov --nonZero
environment:
CODECOV_TOKEN:
from_secret: codecov_token
depends_on:
- python39-pytest
- python310-pytest
- python311-pytest
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- lint
---
kind: pipeline
name: build-package
platform:
os: linux
arch: amd64
steps:
- name: build
image: python:3.11
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: checksum
image: alpine
commands:
- cd dist/ && sha256sum * > ../sha256sum.txt
- name: changelog-generate
image: thegeeklab/git-chglog
commands:
- git fetch -tq
- git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased}
- name: changelog-format
image: thegeeklab/alpine-tools
commands:
- prettier CHANGELOG.md
- prettier -w CHANGELOG.md
- name: publish-github
image: plugins/github-release
settings:
api_key:
from_secret: github_token
files:
- dist/*
- sha256sum.txt
note: CHANGELOG.md
overwrite: true
title: ${DRONE_TAG}
when:
ref:
- refs/tags/**
- name: publish-pypi
image: python:3.11
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry publish -n
environment:
POETRY_HTTP_BASIC_PYPI_PASSWORD:
from_secret: pypi_password
POETRY_HTTP_BASIC_PYPI_USERNAME:
from_secret: pypi_username
when:
ref:
- refs/tags/**
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- test
---
kind: pipeline
name: build-container
platform:
os: linux
arch: amd64
steps:
- name: build
image: python:3.11
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: dryrun
image: thegeeklab/drone-docker-buildx:23
settings:
dockerfile: Dockerfile.multiarch
dry_run: true
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: thegeeklab/${DRONE_REPO_NAME}
when:
ref:
- refs/pull/**
depends_on:
- build
- name: publish-dockerhub
image: thegeeklab/drone-docker-buildx:23
settings:
auto_tag: true
dockerfile: Dockerfile.multiarch
password:
from_secret: docker_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
- name: publish-quay
image: thegeeklab/drone-docker-buildx:23
settings:
auto_tag: true
dockerfile: Dockerfile.multiarch
password:
from_secret: quay_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
registry: quay.io
repo: quay.io/thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: quay_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- test
---
kind: pipeline
name: docs
platform:
os: linux
arch: amd64
concurrency:
limit: 1
steps:
- name: assets
image: thegeeklab/alpine-tools
commands:
- make doc
- name: markdownlint
image: thegeeklab/markdownlint-cli
commands:
- markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md'
- name: spellcheck
image: thegeeklab/alpine-tools
commands:
- spellchecker --files 'docs/content/**/*.md' 'README.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions
environment:
FORCE_COLOR: true
NPM_CONFIG_LOGLEVEL: error
- name: testbuild
image: thegeeklab/hugo:0.114.0
commands:
- hugo --panicOnWarning -s docs/ -b http://localhost:8000/
- name: link-validation
image: thegeeklab/link-validator
commands:
- link-validator --color=always --rate-limit 10
environment:
LINK_VALIDATOR_BASE_DIR: docs/public
LINK_VALIDATOR_RETRIES: 3
- name: build
image: thegeeklab/hugo:0.114.0
commands:
- hugo --panicOnWarning -s docs/
- name: beautify
image: thegeeklab/alpine-tools
commands:
- html-beautify -r -f 'docs/public/**/*.html'
environment:
FORCE_COLOR: true
NPM_CONFIG_LOGLEVEL: error
- name: publish
image: thegeeklab/drone-s3-sync
settings:
access_key:
from_secret: s3_access_key
bucket: geekdocs
delete: true
endpoint: https://sp.rknet.org
path_style: true
secret_key:
from_secret: s3_secret_access_key
source: docs/public/
strip_prefix: docs/public/
target: /${DRONE_REPO_NAME}
when:
ref:
- refs/heads/main
- refs/tags/**
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- build-package
- build-container
---
kind: pipeline
name: notifications
platform:
os: linux
arch: amd64
steps:
- name: pushrm-dockerhub
image: chko/docker-pushrm:1
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: thegeeklab/${DRONE_REPO_NAME}
when:
status:
- success
- name: pushrm-quay
image: chko/docker-pushrm:1
environment:
APIKEY__QUAY_IO:
from_secret: quay_token
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/thegeeklab/${DRONE_REPO_NAME}
when:
status:
- success
- name: matrix
image: thegeeklab/drone-matrix
settings:
homeserver:
from_secret: matrix_homeserver
password:
from_secret: matrix_password
roomid:
from_secret: matrix_roomid
template: "Status: **{{ .Build.Status }}**<br/> Build: [{{ .Repo.Owner }}/{{ .Repo.Name }}]({{ .Build.Link }}){{ if .Build.Branch }} ({{ .Build.Branch }}){{ end }} by {{ .Commit.Author }}<br/> Message: {{ .Commit.Message.Title }}"
username:
from_secret: matrix_username
when:
status:
- success
- failure
trigger:
ref:
- refs/heads/main
- refs/tags/**
status:
- success
- failure
depends_on:
- docs
---
kind: signature
hmac: ae3636efacf23f1ea71a6cfadf90e2010a61daef4625640863bb0d8cb650ada6
...

View File

@ -52,11 +52,7 @@ branches:
required_status_checks: required_status_checks:
strict: false strict: false
contexts: contexts:
- ci/woodpecker/pr/lint - continuous-integration/drone/pr
- ci/woodpecker/pr/test enforce_admins: true
- ci/woodpecker/pr/build-package
- ci/woodpecker/pr/build-container
- ci/woodpecker/pr/docs
enforce_admins: false
required_linear_history: true required_linear_history: true
restrictions: null restrictions: null

View File

@ -1,47 +0,0 @@
---
version: "1.1"
versioning:
update-major: []
update-minor: [feat]
update-patch: [fix, perf, refactor, chore, test, ci, docs]
tag:
pattern: "v%d.%d.%d"
release-notes:
sections:
- name: Features
commit-types: [feat]
section-type: commits
- name: Bug Fixes
commit-types: [fix]
section-type: commits
- name: Performance Improvements
commit-types: [perf]
section-type: commits
- name: Code Refactoring
commit-types: [refactor]
section-type: commits
- name: Others
commit-types: [chore]
section-type: commits
- name: Testing
commit-types: [test]
section-type: commits
- name: CI Pipeline
commit-types: [ci]
section-type: commits
- name: Documentation
commit-types: [docs]
section-type: commits
- name: Breaking Changes
section-type: breaking-changes
commit-message:
footer:
issue:
key: issue
add-value-prefix: "#"
issue:
regex: "#?[0-9]+"

View File

@ -1 +0,0 @@
https://hub.docker.com/r/thegeeklab/*

View File

@ -1,2 +1,3 @@
.drone.yml
*.tpl.md *.tpl.md
LICENSE LICENSE

View File

@ -1,73 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: build
image: docker.io/library/python:3.12
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: dryrun
image: quay.io/thegeeklab/wp-docker-buildx:5
settings:
containerfile: Containerfile.multiarch
dry_run: true
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
when:
- event: [pull_request]
- name: publish-dockerhub
image: quay.io/thegeeklab/wp-docker-buildx:5
group: container
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: docker_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
username:
from_secret: docker_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
- name: publish-quay
image: quay.io/thegeeklab/wp-docker-buildx:5
group: container
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: quay_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
registry: quay.io
repo: quay.io/${CI_REPO}
username:
from_secret: quay_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- lint
- test

View File

@ -1,56 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: build
image: docker.io/library/python:3.12
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: checksum
image: quay.io/thegeeklab/alpine-tools
commands:
- cd dist/ && sha256sum * > ../sha256sum.txt
- name: changelog
image: quay.io/thegeeklab/git-sv
commands:
- git sv current-version
- git sv release-notes -t ${CI_COMMIT_TAG:-next} -o CHANGELOG.md
- cat CHANGELOG.md
- name: publish-github
image: docker.io/plugins/github-release
settings:
api_key:
from_secret: github_token
files:
- dist/*
- sha256sum.txt
note: CHANGELOG.md
overwrite: true
title: ${CI_COMMIT_TAG}
when:
- event: [tag]
- name: publish-pypi
image: docker.io/library/python:3.12
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
when:
- event: [tag]
depends_on:
- lint
- test

View File

@ -1,100 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: assets
image: quay.io/thegeeklab/alpine-tools
commands:
- make doc
- name: markdownlint
image: quay.io/thegeeklab/markdownlint-cli
group: test
commands:
- markdownlint 'README.md' 'CONTRIBUTING.md'
- name: spellcheck
image: quay.io/thegeeklab/alpine-tools
group: test
commands:
- spellchecker --files 'docs/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls
environment:
FORCE_COLOR: "true"
- name: link-validation
image: docker.io/lycheeverse/lychee
group: test
commands:
- lychee --no-progress --format detailed docs/content README.md
- name: build
image: quay.io/thegeeklab/hugo:0.133.0
commands:
- hugo --panicOnWarning -s docs/
- name: beautify
image: quay.io/thegeeklab/alpine-tools
commands:
- html-beautify -r -f 'docs/public/**/*.html'
environment:
FORCE_COLOR: "true"
- name: publish
image: quay.io/thegeeklab/wp-s3-action
settings:
access_key:
from_secret: s3_access_key
bucket: geekdocs
delete: true
endpoint:
from_secret: s3_endpoint
path_style: true
secret_key:
from_secret: s3_secret_access_key
source: docs/public/
strip_prefix: docs/public/
target: /${CI_REPO_NAME}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success, failure]
- name: pushrm-dockerhub
image: docker.io/chko/docker-pushrm:1
secrets:
- source: docker_password
target: DOCKER_PASS
- source: docker_username
target: DOCKER_USER
environment:
PUSHRM_FILE: README.md
PUSHRM_SHORT: Another best practice scanner for Ansible roles and playbooks
PUSHRM_TARGET: ${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
- name: pushrm-quay
image: docker.io/chko/docker-pushrm:1
secrets:
- source: quay_token
target: APIKEY__QUAY_IO
environment:
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
depends_on:
- build-package
- build-container

View File

@ -1,25 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: check-format
image: docker.io/library/python:3.12
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
- poetry run ruff format --check --diff ./${CI_REPO_NAME//-/}
environment:
PY_COLORS: "1"
- name: check-coding
image: docker.io/library/python:3.12
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run ruff check ./${CI_REPO_NAME//-/}
environment:
PY_COLORS: "1"

View File

@ -1,26 +0,0 @@
---
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
runs_on: [success, failure]
steps:
- name: matrix
image: quay.io/thegeeklab/wp-matrix
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
when:
- status: [success, failure]
depends_on:
- docs

View File

@ -1,35 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
variables:
- &pytest_base
group: pytest
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install -E ansible-core
- poetry run pytest --cov-append
- poetry version
- poetry run ${CI_REPO_NAME} --help
environment:
PY_COLORS: "1"
steps:
- name: python-312
image: docker.io/library/python:3.12
<<: *pytest_base
- name: python-311
image: docker.io/library/python:3.11
<<: *pytest_base
- name: python-310
image: docker.io/library/python:3.10
<<: *pytest_base
- name: python-39
image: docker.io/library/python:3.9
<<: *pytest_base

View File

@ -1,4 +1,4 @@
FROM python:3.12-alpine@sha256:7130f75b1bb16c7c5d802782131b4024fe3d7a87ce7d936e8948c2d2e0180bc4 FROM python:3.11-alpine@sha256:25df32b602118dab046b58f0fe920e3301da0727b5b07430c8bcd4b139627fdc
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>" LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="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 # renovate: datasource=github-releases depName=thegeeklab/hugo-geekdoc
THEME_VERSION := v1.0.0 THEME_VERSION := v0.39.5
THEME := hugo-geekdoc THEME := hugo-geekdoc
BASEDIR := docs BASEDIR := docs
THEMEDIR := $(BASEDIR)/themes THEMEDIR := $(BASEDIR)/themes

View File

@ -2,12 +2,13 @@
Another best practice scanner for Ansible roles and playbooks Another best practice scanner for Ansible roles and playbooks
[![Build Status](https://ci.thegeeklab.de/api/badges/thegeeklab/ansible-later/status.svg)](https://ci.thegeeklab.de/repos/thegeeklab/ansible-later) [![Build Status](https://img.shields.io/drone/build/thegeeklab/ansible-later?logo=drone&server=https%3A%2F%2Fdrone.thegeeklab.de)](https://drone.thegeeklab.de/thegeeklab/ansible-later)
[![Docker Hub](https://img.shields.io/badge/dockerhub-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/ansible-later) [![Docker Hub](https://img.shields.io/badge/dockerhub-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/ansible-later)
[![Quay.io](https://img.shields.io/badge/quay-latest-blue.svg?logo=docker&logoColor=white)](https://quay.io/repository/thegeeklab/ansible-later) [![Quay.io](https://img.shields.io/badge/quay-latest-blue.svg?logo=docker&logoColor=white)](https://quay.io/repository/thegeeklab/ansible-later)
[![Python Version](https://img.shields.io/pypi/pyversions/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![Python Version](https://img.shields.io/pypi/pyversions/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![PyPI Status](https://img.shields.io/pypi/status/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![PyPI Status](https://img.shields.io/pypi/status/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![PyPI Release](https://img.shields.io/pypi/v/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![PyPI Release](https://img.shields.io/pypi/v/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![Codecov](https://img.shields.io/codecov/c/github/thegeeklab/ansible-later)](https://codecov.io/gh/thegeeklab/ansible-later)
[![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/graphs/contributors)
[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/ansible-later) [![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) [![License: MIT](https://img.shields.io/github/license/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/blob/main/LICENSE)
@ -18,6 +19,16 @@ ansible-later does **not** ensure that your role will work as expected. For depl
You can find the full documentation at [https://ansible-later.geekdocs.de](https://ansible-later.geekdocs.de/). 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 ## Contributors
Special thanks to all [contributors](https://github.com/thegeeklab/ansible-later/graphs/contributors). If you would like to contribute, 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 import LOG, __version__, logger
from ansiblelater.candidate import Candidate from ansiblelater.candidate import Candidate
from ansiblelater.rule import SingleRules
from ansiblelater.settings import Settings from ansiblelater.settings import Settings
from ansiblelater.standard import SingleStandards
def main(): def main():
@ -22,33 +22,33 @@ def main():
parser.add_argument( parser.add_argument(
"-r", "-r",
"--rules-dir", "--rules-dir",
dest="rules.dir", dest="rules.standards",
metavar="DIR", metavar="RULES",
action="append", action="append",
help="directory of rules", help="directory of standard rules"
) )
parser.add_argument( parser.add_argument(
"-B", "-B",
"--no-builtin", "--no-buildin",
dest="rules.builtin", dest="rules.buildin",
action="store_false", action="store_false",
help="disables built-in rules", help="disables build-in standard rules"
) )
parser.add_argument( parser.add_argument(
"-i", "-s",
"--include-rules", "--standards",
dest="rules.include_filter", dest="rules.filter",
metavar="TAGS", metavar="FILTER",
action="append", action="append",
help="limit rules to given id/tags", help="limit standards to given ID's"
) )
parser.add_argument( parser.add_argument(
"-x", "-x",
"--exclude-rules", "--exclude-standards",
dest="rules.exclude_filter", dest="rules.exclude_filter",
metavar="TAGS", metavar="EXCLUDE_FILTER",
action="append", action="append",
help="exclude rules by given it/tags", help="exclude standards by given ID's"
) )
parser.add_argument( parser.add_argument(
"-v", dest="logging.level", action="append_const", const=-1, help="increase log level" "-v", dest="logging.level", action="append_const", const=-1, help="increase log level"
@ -65,7 +65,7 @@ def main():
config = settings.config config = settings.config
logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"]) 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) workers = max(multiprocessing.cpu_count() - 2, 2)
p = multiprocessing.Pool(workers) p = multiprocessing.Pool(workers)
@ -85,7 +85,7 @@ def main():
else: else:
LOG.info(f"Couldn't classify file {filename}") LOG.info(f"Couldn't classify file {filename}")
errors = sum(p.map(_review_wrapper, tasks)) errors = (sum(p.map(_review_wrapper, tasks)))
p.close() p.close()
p.join() p.join()

View File

@ -3,12 +3,14 @@
import codecs import codecs
import copy import copy
import os import os
import re
from distutils.version import LooseVersion
from ansible.plugins.loader import module_loader from ansible.plugins.loader import module_loader
from ansiblelater import LOG from ansiblelater import LOG, utils
from ansiblelater.logger import flag_extra from ansiblelater.logger import flag_extra
from ansiblelater.rule import RuleBase, SingleRules from ansiblelater.standard import SingleStandards, StandardBase
class Candidate: class Candidate:
@ -19,12 +21,11 @@ class Candidate:
bundled with necessary meta informations for rule processing. 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.path = filename
self.binary = False self.binary = False
self.vault = False self.vault = False
self.filemeta = type(self).__name__.lower() self.filetype = type(self).__name__.lower()
self.kind = type(self).__name__.lower()
self.faulty = False self.faulty = False
self.config = settings.config self.config = settings.config
self.settings = settings self.settings = settings
@ -36,58 +37,107 @@ class Candidate:
except UnicodeDecodeError: except UnicodeDecodeError:
self.binary = True self.binary = True
def _filter_rules(self): def _get_version(self):
target_rules = [] name = type(self).__name__
includes = self.config["rules"]["include_filter"] 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"] excludes = self.config["rules"]["exclude_filter"]
if len(includes) == 0: 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: for standard in self.standards:
if rule.rid in includes and rule.rid not in excludes: if standard.sid in includes and standard.sid not in excludes:
target_rules.append(rule) target_standards.append(standard)
return target_rules return target_standards
def review(self): def review(self):
errors = 0 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(): for standard in self._filter_standards():
if self.kind not in rule.types: if type(self).__name__.lower() not in standard.types:
continue continue
result = rule.check(self, self.config) result = standard.check(self, self.config)
if not result: 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 continue
labels = { labels = {
"tag": "review", "tag": "review",
"rule": rule.description, "standard": standard.description,
"file": self.path, "file": self.path,
"passed": True, "passed": True
} }
if rule.rid and rule.rid.strip(): if standard.sid and standard.sid.strip():
labels["rid"] = rule.rid labels["sid"] = standard.sid
for err in result.errors: for err in result.errors:
err_labels = copy.copy(labels) err_labels = copy.copy(labels)
err_labels["passed"] = False err_labels["passed"] = False
rid = self._format_id(rule.rid) sid = self._format_id(standard.sid)
path = self.path 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()) err_labels.update(err.to_dict())
msg = f"{rid}rule '{description}' not met:\n{path}:{err}" if not standard.version:
LOG.warning(
f"{sid}Best practice '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels)
)
elif LooseVersion(standard.version) > LooseVersion(self.version):
LOG.warning(
f"{sid}Future standard '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels)
)
else:
msg = f"{sid}Standard '{description}' not met:\n{path}:{err}"
if rule.rid not in self.config["rules"]["warning_filter"]: if standard.sid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels)) LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1 errors = errors + 1
else: else:
@ -96,57 +146,55 @@ class Candidate:
return errors return errors
@staticmethod @staticmethod
def classify(filename, settings={}, rules=[]): # noqa def classify(filename, settings={}, standards=[]): # noqa
parentdir = os.path.basename(os.path.dirname(filename)) parentdir = os.path.basename(os.path.dirname(filename))
basename = os.path.basename(filename) basename = os.path.basename(filename)
ext = os.path.splitext(filename)[1][1:] ext = os.path.splitext(filename)[1][1:]
if parentdir in ["tasks"]: if parentdir in ["tasks"]:
return Task(filename, settings, rules) return Task(filename, settings, standards)
if parentdir in ["handlers"]: if parentdir in ["handlers"]:
return Handler(filename, settings, rules) return Handler(filename, settings, standards)
if parentdir in ["vars", "defaults"]: if parentdir in ["vars", "defaults"]:
return RoleVars(filename, settings, rules) return RoleVars(filename, settings, standards)
if "group_vars" in filename.split(os.sep): 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): 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: 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: if parentdir in ["meta"] and "argument_specs" in basename:
return ArgumentSpecs(filename, settings, rules) return ArgumentSpecs(filename, settings, standards)
if parentdir in [ if (
"library", parentdir in ["library", "lookup_plugins", "callback_plugins", "filter_plugins"]
"lookup_plugins", or filename.endswith(".py")
"callback_plugins", ):
"filter_plugins", return Code(filename, settings, standards)
] or filename.endswith(".py"):
return Code(filename, settings, rules)
if basename == "inventory" or basename == "hosts" or parentdir in ["inventories"]: 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"]): 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: 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"): 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): 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"): if basename.endswith(".yml") or basename.endswith(".yaml"):
return Playbook(filename, settings, rules) return Playbook(filename, settings, standards)
if "README" in basename: if "README" in basename:
return Doc(filename, settings, rules) return Doc(filename, settings, standards)
return None return None
def _format_id(self, rule_id): def _format_id(self, standard_id):
rid = rule_id.strip() sid = standard_id.strip()
if rid: if sid:
rule_id = f"[{rid}] " standard_id = f"[{sid}] "
return rule_id return standard_id
def __repr__(self): def __repr__(self):
return f"{self.kind} ({self.path})" return f"{type(self).__name__} ({self.path})"
def __getitem__(self, item): def __getitem__(self, item):
return self.__dict__.get(item) return self.__dict__.get(item)
@ -155,8 +203,8 @@ class Candidate:
class RoleFile(Candidate): class RoleFile(Candidate):
"""Object classified as Ansible role file.""" """Object classified as Ansible role file."""
def __init__(self, filename, settings={}, rules=[]): # noqa def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, rules) super().__init__(filename, settings, standards)
parentdir = os.path.dirname(os.path.abspath(filename)) parentdir = os.path.dirname(os.path.abspath(filename))
while parentdir != os.path.dirname(parentdir): while parentdir != os.path.dirname(parentdir):
@ -176,17 +224,17 @@ class Playbook(Candidate):
class Task(RoleFile): class Task(RoleFile):
"""Object classified as Ansible task file.""" """Object classified as Ansible task file."""
def __init__(self, filename, settings={}, rules=[]): # noqa def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, rules) super().__init__(filename, settings, standards)
self.filemeta = "tasks" self.filetype = "tasks"
class Handler(RoleFile): class Handler(RoleFile):
"""Object classified as Ansible handler file.""" """Object classified as Ansible handler file."""
def __init__(self, filename, settings={}, rules=[]): # noqa def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, rules) super().__init__(filename, settings, standards)
self.filemeta = "handlers" self.filetype = "handlers"
class Vars(Candidate): class Vars(Candidate):

View File

@ -3,6 +3,7 @@
import logging import logging
import os import os
import sys import sys
from distutils.util import strtobool
import colorama import colorama
from pythonjsonlogger import jsonlogger from pythonjsonlogger import jsonlogger
@ -11,35 +12,12 @@ CONSOLE_FORMAT = "{}%(levelname)s:{} %(message)s"
JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s" JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s"
def strtobool(value):
"""Convert a string representation of truth to true or false."""
_map = {
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
try:
return _map[str(value).lower()]
except KeyError as err:
raise ValueError(f'"{value}" is not a valid bool value') from err
def to_bool(string): def to_bool(string):
return bool(strtobool(str(string))) return bool(strtobool(str(string)))
def _should_do_markup(): def _should_do_markup():
py_colors = os.environ.get("PY_COLORS", None) py_colors = os.environ.get("PY_COLORS", None)
if py_colors is not None: if py_colors is not None:
return to_bool(py_colors) return to_bool(py_colors)
@ -82,7 +60,7 @@ class LogFilter:
class MultilineFormatter(logging.Formatter): class MultilineFormatter(logging.Formatter):
"""Logging Formatter to reset color after newline characters.""" """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.replace("\n", f"\n{colorama.Style.RESET_ALL}... ")
record.msg = record.msg + "\n" record.msg = record.msg + "\n"
return logging.Formatter.format(self, record) return logging.Formatter.format(self, record)
@ -91,7 +69,7 @@ class MultilineFormatter(logging.Formatter):
class MultilineJsonFormatter(jsonlogger.JsonFormatter): class MultilineJsonFormatter(jsonlogger.JsonFormatter):
"""Logging Formatter to remove newline characters.""" """Logging Formatter to remove newline characters."""
def format(self, record): def format(self, record): # noqa
record.msg = record.msg.replace("\n", " ") record.msg = record.msg.replace("\n", " ")
return jsonlogger.JsonFormatter.format(self, record) return jsonlogger.JsonFormatter.format(self, record)

View File

@ -1,10 +1,12 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckBecomeUser(RuleBase): class CheckBecomeUser(StandardBase):
rid = "ANS115"
sid = "ANSIBLE0015"
description = "Become should be combined with become_user" description = "Become should be combined with become_user"
helptext = "the task has `become` enabled but `become_user` is missing" helptext = "the task has `become` enabled but `become_user` is missing"
version = "0.1"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -14,7 +16,7 @@ class CheckBecomeUser(RuleBase):
if not errors: if not errors:
gen = (task for task in tasks if "become" in task) gen = (task for task in tasks if "become" in task)
for task in gen: for task in gen:
if task["become"] in true_value and "become_user" not in task: if task["become"] in true_value and "become_user" not in task.keys():
errors.append(self.Error(task["__line__"], self.helptext)) errors.append(self.Error(task["__line__"], self.helptext))
return self.Result(candidate.path, errors) return self.Result(candidate.path, errors)

View File

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

View File

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

View File

@ -1,13 +1,15 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckCommandHasChanges(RuleBase): class CheckCommandHasChanges(StandardBase):
rid = "ANS111"
sid = "ANSIBLE0011"
description = "Commands should be idempotent" description = "Commands should be idempotent"
helptext = ( helptext = (
"commands should only read while using `changed_when` or try to be " "commands should only read while using `changed_when` or try to be "
"idempotent while using controls like `creates`, `removes` or `when`" "idempotent while using controls like `creates`, `removes` or `when`"
) )
version = "0.1"
types = ["playbook", "task"] types = ["playbook", "task"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -17,11 +19,9 @@ class CheckCommandHasChanges(RuleBase):
if not errors: if not errors:
for task in tasks: for task in tasks:
if task["action"]["__ansible_module__"] in commands and ( if task["action"]["__ansible_module__"] in commands and (
"changed_when" not in task "changed_when" not in task and "when" not in task
and "when" not in task
and "when" not in task.get("__ansible_action_meta__", []) and "when" not in task.get("__ansible_action_meta__", [])
and "creates" not in task["action"] and "creates" not in task["action"] and "removes" not in task["action"]
and "removes" not in task["action"]
): ):
errors.append(self.Error(task["__line__"], self.helptext)) errors.append(self.Error(task["__line__"], self.helptext))

View File

@ -20,13 +20,15 @@
import os import os
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckCommandInsteadOfArgument(RuleBase): class CheckCommandInsteadOfArgument(StandardBase):
rid = "ANS117"
sid = "ANSIBLE0017"
description = "Commands should not be used in place of module arguments" description = "Commands should not be used in place of module arguments"
helptext = "{exec} used in place of file modules argument {arg}" helptext = "{exec} used in place of file modules argument {arg}"
version = "0.2"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -39,7 +41,7 @@ class CheckCommandInsteadOfArgument(RuleBase):
"ln": "state=link", "ln": "state=link",
"mkdir": "state=directory", "mkdir": "state=directory",
"rmdir": "state=absent", "rmdir": "state=absent",
"rm": "state=absent", "rm": "state=absent"
} }
if not errors: if not errors:
@ -49,14 +51,13 @@ class CheckCommandInsteadOfArgument(RuleBase):
executable = os.path.basename(first_cmd_arg) executable = os.path.basename(first_cmd_arg)
if ( if (
first_cmd_arg first_cmd_arg and executable in arguments
and executable in arguments
and task["action"].get("warn", True) and task["action"].get("warn", True)
): ):
errors.append( errors.append(
self.Error( self.Error(
task["__line__"], task["__line__"],
self.helptext.format(exec=executable, arg=arguments[executable]), self.helptext.format(exec=executable, arg=arguments[executable])
) )
) )

View File

@ -1,12 +1,14 @@
import os import os
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckCommandInsteadOfModule(RuleBase): class CheckCommandInsteadOfModule(StandardBase):
rid = "ANS108"
sid = "ANSIBLE0008"
description = "Commands should not be used in place of modules" description = "Commands should not be used in place of modules"
helptext = "{exec} command used in place of {module} module" helptext = "{exec} command used in place of {module} module"
version = "0.1"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -29,7 +31,7 @@ class CheckCommandInsteadOfModule(RuleBase):
"rsync": "synchronize", "rsync": "synchronize",
"supervisorctl": "supervisorctl", "supervisorctl": "supervisorctl",
"systemctl": "systemd", "systemctl": "systemd",
"sed": "template or lineinfile", "sed": "template or lineinfile"
} }
if not errors: if not errors:
@ -40,16 +42,14 @@ class CheckCommandInsteadOfModule(RuleBase):
cmd = cmd = self.get_safe_cmd(task) cmd = cmd = self.get_safe_cmd(task)
if ( if (
first_cmd_arg first_cmd_arg and executable in modules
and executable in modules and task["action"].get("warn", True) and "register" not in task
and task["action"].get("warn", True)
and "register" not in task
and not any(ch in cmd for ch in self.SHELL_PIPE_CHARS) and not any(ch in cmd for ch in self.SHELL_PIPE_CHARS)
): ):
errors.append( errors.append(
self.Error( self.Error(
task["__line__"], task["__line__"],
self.helptext.format(exec=executable, module=modules[executable]), self.helptext.format(exec=executable, module=modules[executable])
) )
) )

View File

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

View File

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

View File

@ -1,10 +1,12 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckDeprecated(RuleBase): class CheckDeprecated(StandardBase):
rid = "ANS999"
sid = "ANSIBLE9999"
description = "Deprecated features should not be used" 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -18,7 +20,7 @@ class CheckDeprecated(RuleBase):
task["__line__"], task["__line__"],
self.helptext.format( self.helptext.format(
old="skip_ansible_lint", new="skip_ansible_later" old="skip_ansible_lint", new="skip_ansible_later"
), )
) )
) )
return self.Result(candidate.path, errors) return self.Result(candidate.path, errors)

View File

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

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,18 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckFilePermissionMissing(RuleBase): class CheckFilePermissionMissing(StandardBase):
rid = "ANS118"
sid = "ANSIBLE0018"
description = "File permissions unset or incorrect" description = "File permissions unset or incorrect"
helptext = ( helptext = (
"`mode` parameter should set permissions explicitly (e.g. `mode: 0644`) " "`mode` parameter should set permissions explicitly (e.g. `mode: 0644`) "
"to avoid unexpected file permissions" "to avoid unexpected file permissions"
) )
version = "0.2"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
_modules = { _modules = {
@ -65,7 +67,8 @@ class CheckFilePermissionMissing(RuleBase):
mode = task["action"].get("mode", None) mode = task["action"].get("mode", None)
state = task["action"].get("state", "file") state = task["action"].get("state", "file")
if module not in self._modules and module not in self._create_modules: if module not in self._modules and \
module not in self._create_modules:
return False return False
if mode == "preserve" and module not in self._preserve_modules: if mode == "preserve" and module not in self._preserve_modules:

View File

@ -18,27 +18,22 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckFilePermissionOctal(RuleBase): class CheckFilePermissionOctal(StandardBase):
rid = "ANS119"
description = "Numeric file permissions without a leading zero can behave unexpectedly" sid = "ANSIBLE0019"
helptext = '`mode: {mode}` should be strings with a leading zero `mode: "0{mode}"`' 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
tasks, errors = self.get_normalized_tasks(candidate, settings) tasks, errors = self.get_normalized_tasks(candidate, settings)
modules = [ modules = [
"assemble", "assemble", "copy", "file", "ini_file", "lineinfile", "replace", "synchronize",
"copy", "template", "unarchive"
"file",
"ini_file",
"lineinfile",
"replace",
"synchronize",
"template",
"unarchive",
] ]
if not errors: if not errors:
@ -47,32 +42,26 @@ class CheckFilePermissionOctal(RuleBase):
mode = task["action"].get("mode", None) mode = task["action"].get("mode", None)
if isinstance(mode, int) and self._is_invalid_permission(mode): if isinstance(mode, int) and self._is_invalid_permission(mode):
errors.append( errors.append(self.Error(task["__line__"], self.helptext))
self.Error(task["__line__"], self.helptext.format(mode=mode))
)
return self.Result(candidate.path, errors) return self.Result(candidate.path, errors)
@staticmethod @staticmethod
def _is_invalid_permission(mode): def _is_invalid_permission(mode):
other_write_without_read = ( other_write_without_read = (
mode % 8 and mode % 8 < 4 and not (mode % 8 == 1 and (mode >> 6) % 2 == 1) mode % 8 and mode % 8 < 4 and not (mode % 8 == 1 and (mode >> 6) % 2 == 1)
) )
group_write_without_read = ( group_write_without_read = ((mode >> 3) % 8 and (mode >> 3) % 8 < 4
(mode >> 3) % 8 and not ((mode >> 3) % 8 == 1 and (mode >> 6) % 2 == 1))
and (mode >> 3) % 8 < 4 user_write_without_read = ((mode >> 6) % 8 and (mode >> 6) % 8 < 4
and not ((mode >> 3) % 8 == 1 and (mode >> 6) % 2 == 1) and (mode >> 6) % 8 != 1)
)
user_write_without_read = (mode >> 6) % 8 and (mode >> 6) % 8 < 4 and (mode >> 6) % 8 != 1
other_more_generous_than_group = mode % 8 > (mode >> 3) % 8 other_more_generous_than_group = mode % 8 > (mode >> 3) % 8
other_more_generous_than_user = mode % 8 > (mode >> 6) % 8 other_more_generous_than_user = mode % 8 > (mode >> 6) % 8
group_more_generous_than_user = (mode >> 3) % 8 > (mode >> 6) % 8 group_more_generous_than_user = (mode >> 3) % 8 > (mode >> 6) % 8
return bool( return bool(
other_write_without_read other_write_without_read or group_write_without_read or user_write_without_read
or group_write_without_read or other_more_generous_than_group or other_more_generous_than_user
or user_write_without_read
or other_more_generous_than_group
or other_more_generous_than_user
or group_more_generous_than_user or group_more_generous_than_user
) )

View File

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

View File

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

View File

@ -1,41 +1,21 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckInstallUseLatest(RuleBase): class CheckInstallUseLatest(StandardBase):
rid = "ANS109"
sid = "ANSIBLE0009"
description = "Package installs should use present, not latest" description = "Package installs should use present, not latest"
helptext = "package installs should use `state=present` with or without a version" helptext = "package installs should use `state=present` with or without a version"
version = "0.1"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
tasks, errors = self.get_normalized_tasks(candidate, settings) tasks, errors = self.get_normalized_tasks(candidate, settings)
package_managers = [ package_managers = [
"yum", "yum", "apt", "dnf", "homebrew", "pacman", "openbsd_package", "pkg5", "portage",
"apt", "pkgutil", "slackpkg", "swdepot", "zypper", "bundler", "pip", "pear", "npm", "yarn",
"dnf", "gem", "easy_install", "bower", "package", "apk", "openbsd_pkg", "pkgng", "sorcery",
"homebrew", "xbps"
"pacman",
"openbsd_package",
"pkg5",
"portage",
"pkgutil",
"slackpkg",
"swdepot",
"zypper",
"bundler",
"pip",
"pear",
"npm",
"yarn",
"gem",
"easy_install",
"bower",
"package",
"apk",
"openbsd_pkg",
"pkgng",
"sorcery",
"xbps",
] ]
if not errors: if not errors:

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,14 @@
import re import re
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckLiteralBoolFormat(RuleBase): class CheckLiteralBoolFormat(StandardBase):
rid = "ANS114"
sid = "ANSIBLE0014"
description = "Literal bools should be consistent" description = "Literal bools should be consistent"
helptext = "literal bools should be written as `{bools}`" helptext = "literal bools should be written as `{bools}`"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"] types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings): def check(self, candidate, settings):

View File

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

View File

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

View File

@ -1,12 +1,14 @@
from nested_lookup import nested_lookup from nested_lookup import nested_lookup
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckMetaMain(RuleBase): class CheckMetaMain(StandardBase):
rid = "ANS102"
sid = "ANSIBLE0002"
description = "Roles must contain suitable meta/main.yml" description = "Roles must contain suitable meta/main.yml"
helptext = "file should contain `{key}` key" helptext = "file should contain `{key}` key"
version = "0.1"
types = ["meta"] types = ["meta"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -14,8 +16,8 @@ class CheckMetaMain(RuleBase):
keys = ["author", "description", "min_ansible_version", "platforms"] keys = ["author", "description", "min_ansible_version", "platforms"]
if not errors: if not errors:
has_galaxy_info = isinstance(content, dict) and "galaxy_info" in content has_galaxy_info = (isinstance(content, dict) and "galaxy_info" in content)
has_dependencies = isinstance(content, dict) and "dependencies" in content has_dependencies = (isinstance(content, dict) and "dependencies" in content)
if not has_galaxy_info: if not has_galaxy_info:
errors.append(self.Error(None, self.helptext.format(key="galaxy_info"))) errors.append(self.Error(None, self.helptext.format(key="galaxy_info")))

View File

@ -1,12 +1,14 @@
from collections import defaultdict from collections import defaultdict
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckNameFormat(RuleBase): class CheckNameFormat(StandardBase):
rid = "ANS107"
sid = "ANSIBLE0007"
description = "Name of tasks and handlers must be formatted" 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -17,7 +19,7 @@ class CheckNameFormat(RuleBase):
for task in tasks: for task in tasks:
if "name" in task: if "name" in task:
namelines[task["name"]].append(task["__line__"]) namelines[task["name"]].append(task["__line__"])
for name, lines in namelines.items(): for (name, lines) in namelines.items():
if name and not name[0].isupper(): if name and not name[0].isupper():
errors.append(self.Error(lines[-1], self.helptext.format(name=name))) errors.append(self.Error(lines[-1], self.helptext.format(name=name)))

View File

@ -1,10 +1,12 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckNamedTask(RuleBase): class CheckNamedTask(StandardBase):
rid = "ANS106"
sid = "ANSIBLE0006"
description = "Tasks and handlers must be named" 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):

View File

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

View File

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

View File

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

View File

@ -1,12 +1,14 @@
from ansible.parsing.yaml.objects import AnsibleMapping from ansible.parsing.yaml.objects import AnsibleMapping
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckScmInSrc(RuleBase): class CheckScmInSrc(StandardBase):
rid = "ANS105"
sid = "ANSIBLE0005"
description = "Use `scm:` key rather than `src: scm+url`" description = "Use `scm:` key rather than `src: scm+url`"
helptext = "usage of `src: scm+url` not recommended" helptext = "usage of `src: scm+url` not recommended"
version = "0.1"
types = ["rolesfile"] types = ["rolesfile"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -15,8 +17,7 @@ class CheckScmInSrc(RuleBase):
if not errors: if not errors:
for role in roles: for role in roles:
if ( if (
isinstance(role, AnsibleMapping) isinstance(role, AnsibleMapping) and bool(role.get("src"))
and bool(role.get("src"))
and "+" in role.get("src") and "+" in role.get("src")
): ):
errors.append(self.Error(role["__line__"], self.helptext)) errors.append(self.Error(role["__line__"], self.helptext))

View File

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

View File

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

View File

@ -1,12 +1,14 @@
from collections import defaultdict from collections import defaultdict
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckUniqueNamedTask(RuleBase): class CheckUniqueNamedTask(StandardBase):
rid = "ANS103"
sid = "ANSIBLE0003"
description = "Tasks and handlers must be uniquely named within a single file" 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):
@ -18,7 +20,7 @@ class CheckUniqueNamedTask(RuleBase):
for task in tasks: for task in tasks:
if "name" in task: if "name" in task:
namelines[task["name"]].append(task["__line__"]) namelines[task["name"]].append(task["__line__"])
for name, lines in namelines.items(): for (name, lines) in namelines.items():
if name and len(lines) > 1: if name and len(lines) > 1:
errors.append(self.Error(lines[-1], self.helptext.format(name=name))) errors.append(self.Error(lines[-1], self.helptext.format(name=name)))

View File

@ -0,0 +1,17 @@
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,15 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckWhenFormat(RuleBase): class CheckWhenFormat(StandardBase):
rid = "ANS122"
sid = "ANSIBLE0022"
description = "Don't use Jinja2 in when" description = "Don't use Jinja2 in when"
helptext = ( 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"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
from ansiblelater.rule import RuleBase from ansiblelater.standard import StandardBase
class CheckYamlIndent(RuleBase): class CheckYamlIndent(StandardBase):
rid = "YML102"
sid = "LINT0002"
description = "YAML should not contain unnecessarily empty lines" description = "YAML should not contain unnecessarily empty lines"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
def check(self, candidate, settings): 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.""" """Global settings object definition."""
import importlib.resources
import os import os
import anyconfig import anyconfig
@ -8,6 +7,7 @@ import jsonschema.exceptions
import pathspec import pathspec
from appdirs import AppDirs from appdirs import AppDirs
from jsonschema._utils import format_as_index from jsonschema._utils import format_as_index
from pkg_resources import resource_filename
from ansiblelater import utils from ansiblelater import utils
@ -104,13 +104,13 @@ class Settings:
if f not in defaults["ansible"]["custom_modules"]: if f not in defaults["ansible"]["custom_modules"]:
defaults["ansible"]["custom_modules"].append(f) defaults["ansible"]["custom_modules"].append(f)
if defaults["rules"]["builtin"]: if defaults["rules"]["buildin"]:
ref = importlib.resources.files("ansiblelater") / "rules" defaults["rules"]["standards"].append(
with importlib.resources.as_file(ref) as path: os.path.join(resource_filename("ansiblelater", "rules"))
defaults["rules"]["dir"].append(path) )
defaults["rules"]["dir"] = [ defaults["rules"]["standards"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["dir"] os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["standards"]
] ]
return defaults return defaults
@ -118,20 +118,21 @@ class Settings:
def _get_defaults(self): def _get_defaults(self):
defaults = { defaults = {
"rules": { "rules": {
"builtin": True, "buildin": True,
"dir": [], "standards": [],
"include_filter": [], "filter": [],
"exclude_filter": [], "exclude_filter": [],
"warning_filter": [ "warning_filter": [
"ANS128", "ANSIBLE9999",
"ANS999", "ANSIBLE9998",
], ],
"ignore_dotfiles": True, "ignore_dotfiles": True,
"exclude_files": [], "exclude_files": [],
"version": ""
}, },
"logging": { "logging": {
"level": "WARNING", "level": "WARNING",
"json": False, "json": False
}, },
"ansible": { "ansible": {
"custom_modules": [], "custom_modules": [],
@ -144,7 +145,7 @@ class Settings:
"exclude": [ "exclude": [
"meta", "meta",
"debug", "debug",
"block/always/rescue", "block",
"include_role", "include_role",
"import_role", "import_role",
"include_tasks", "include_tasks",
@ -168,21 +169,17 @@ class Settings:
"indent-sequences": True, "indent-sequences": True,
}, },
"hyphens": { "hyphens": {
"max-spaces-after": 1, "max-spaces-after": 1
}, },
"document-start": { "document-start": {
"present": True, "present": True
}, },
"document-end": { "document-end": {
"present": False, "present": True
}, },
"colons": { "colons": {
"max-spaces-before": 0, "max-spaces-before": 0,
"max-spaces-after": 1, "max-spaces-after": 1
},
"octal-values": {
"forbid-implicit-octal": True,
"forbid-explicit-octal": True,
}, },
}, },
} }
@ -198,7 +195,7 @@ class Settings:
except jsonschema.exceptions.ValidationError as e: except jsonschema.exceptions.ValidationError as e:
validator = e.validator validator = e.validator
path = format_as_index( path = format_as_index(
next(iter(e.absolute_path)), list(e.absolute_path)[0],
list(e.absolute_path)[1:], list(e.absolute_path)[1:],
) )
msg = e.message msg = e.message
@ -213,10 +210,9 @@ class Settings:
excludes = self.config["rules"]["exclude_files"] excludes = self.config["rules"]["exclude_files"]
ignore_dotfiles = self.config["rules"]["ignore_dotfiles"] ignore_dotfiles = self.config["rules"]["ignore_dotfiles"]
if ignore_dotfiles: if ignore_dotfiles and not self.args_files:
excludes.append(".*") excludes.append(".*")
else:
if self.args_files:
del excludes[:] del excludes[:]
filelist = [] filelist = []

View File

@ -1,4 +1,4 @@
"""Rule definition.""" """Standard definition."""
import copy import copy
import importlib import importlib
@ -27,26 +27,29 @@ from ansiblelater.utils.yamlhelper import (
) )
class RuleMeta(type): class StandardMeta(type):
def __call__(cls, *args): def __call__(cls, *args):
mcls = type.__call__(cls, *args) mcls = type.__call__(cls, *args)
mcls.rid = cls.rid mcls.sid = cls.sid
mcls.description = getattr(cls, "description", "__unknown__") mcls.description = getattr(cls, "description", "__unknown__")
mcls.helptext = getattr(cls, "helptext", "") mcls.helptext = getattr(cls, "helptext", "")
mcls.version = getattr(cls, "version", None)
mcls.types = getattr(cls, "types", []) mcls.types = getattr(cls, "types", [])
return mcls return mcls
class RuleExtendedMeta(RuleMeta, ABCMeta): class StandardExtendedMeta(StandardMeta, ABCMeta):
pass pass
class RuleBase(metaclass=RuleExtendedMeta): class StandardBase(metaclass=StandardExtendedMeta):
SHELL_PIPE_CHARS = "&|<>;$\n*[]{}?" SHELL_PIPE_CHARS = "&|<>;$\n*[]{}?"
@property @property
@abstractmethod @abstractmethod
def rid(self): def sid(self):
pass pass
@abstractmethod @abstractmethod
@ -54,7 +57,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
pass pass
def __repr__(self): def __repr__(self):
return f"Rule: {self.description} (types: {self.types})" return f"Standard: {self.description} (version: {self.version}, types: {self.types})"
@staticmethod @staticmethod
def get_tasks(candidate, settings): # noqa def get_tasks(candidate, settings): # noqa
@ -68,11 +71,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original
errors.append( 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 candidate.faulty = True
except LaterAnsibleError as e: 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 candidate.faulty = True
return yamllines, errors return yamllines, errors
@ -92,11 +95,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original
errors.append( 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 candidate.faulty = True
except LaterAnsibleError as e: 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 candidate.faulty = True
return tasks, errors return tasks, errors
@ -114,11 +117,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original
errors.append( 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 candidate.faulty = True
except LaterAnsibleError as e: 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 candidate.faulty = True
return normalized, errors return normalized, errors
@ -149,21 +152,20 @@ class RuleBase(metaclass=RuleExtendedMeta):
# No need to normalize_task if we are skipping it. # No need to normalize_task if we are skipping it.
continue continue
normalized_task = normalize_task( normalized.append(
normalize_task(
task, candidate.path, settings["ansible"]["custom_modules"] task, candidate.path, settings["ansible"]["custom_modules"]
) )
normalized_task["__raw_task__"] = task )
normalized.append(normalized_task)
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original
errors.append( 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 candidate.faulty = True
except LaterAnsibleError as e: 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 candidate.faulty = True
return normalized, errors return normalized, errors
@ -184,11 +186,11 @@ class RuleBase(metaclass=RuleExtendedMeta):
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original
errors.append( 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 candidate.faulty = True
except LaterAnsibleError as e: 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 candidate.faulty = True
return yamllines, errors return yamllines, errors
@ -210,7 +212,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
content = yaml.safe_load(f) content = yaml.safe_load(f)
except yaml.YAMLError as e: except yaml.YAMLError as e:
errors.append( 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 candidate.faulty = True
@ -224,14 +226,14 @@ class RuleBase(metaclass=RuleExtendedMeta):
try: try:
with open(candidate.path, encoding="utf-8") as f: with open(candidate.path, encoding="utf-8") as f:
for problem in linter.run(f, YamlLintConfig(options)): 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: except yaml.YAMLError as e:
errors.append( 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 candidate.faulty = True
except (TypeError, ValueError) as e: 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 candidate.faulty = True
return errors return errors
@ -277,7 +279,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
self.lineno = lineno self.lineno = lineno
self.message = message self.message = message
self.kwargs = kwargs self.kwargs = kwargs
for key, value in kwargs.items(): for (key, value) in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
def __repr__(self): def __repr__(self):
@ -287,7 +289,7 @@ class RuleBase(metaclass=RuleExtendedMeta):
def to_dict(self): def to_dict(self):
result = {"lineno": self.lineno, "message": self.message} result = {"lineno": self.lineno, "message": self.message}
for key, value in self.kwargs.items(): for (key, value) in self.kwargs.items():
result[key] = value result[key] = value
return result return result
@ -302,7 +304,8 @@ class RuleBase(metaclass=RuleExtendedMeta):
return "\n".join([f"{self.candidate}:{error}" for error in self.errors]) return "\n".join([f"{self.candidate}:{error}" for error in self.errors])
class RulesLoader: class StandardLoader:
def __init__(self, source): def __init__(self, source):
self.rules = [] self.rules = []
@ -330,21 +333,21 @@ class RulesLoader:
self.validate() self.validate()
def _is_plugin(self, obj): def _is_plugin(self, obj):
return ( return inspect.isclass(obj) and issubclass(
inspect.isclass(obj) and issubclass(obj, RuleBase) and obj is not RuleBase and not None obj, StandardBase
) ) and obj is not StandardBase and not None
def validate(self): def validate(self):
normalize_rule = list(toolz.remove(lambda x: x.rid == "", self.rules)) normalized_std = (list(toolz.remove(lambda x: x.sid == "", self.rules)))
unique_rule = len(list(toolz.unique(normalize_rule, key=lambda x: x.rid))) unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.sid)))
all_rules = len(normalize_rule) all_std = len(normalized_std)
if all_rules != unique_rule: if all_std != unique_std:
sysexit_with_message( 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.""" """Singleton config class."""
pass pass

View File

@ -4,10 +4,9 @@ import contextlib
import re import re
import sys import sys
from contextlib import suppress from contextlib import suppress
from functools import lru_cache from distutils.version import LooseVersion
import yaml import yaml
from ansible.plugins.loader import module_loader
from ansiblelater import logger from ansiblelater import logger
@ -33,7 +32,12 @@ def count_spaces(c_string):
break break
trailing_spaces += 1 trailing_spaces += 1
return (leading_spaces, trailing_spaces) return ((leading_spaces, trailing_spaces))
def standards_latest(standards):
return max([standard.version for standard in standards if standard.version] or ["0.1"],
key=LooseVersion)
def lines_ranges(lines_spec): def lines_ranges(lines_spec):
@ -78,9 +82,11 @@ def open_file(filename, mode="r"):
def add_dict_branch(tree, vector, value): def add_dict_branch(tree, vector, value):
key = vector[0] key = vector[0]
tree[key] = ( tree[key] = value \
value if len(vector) == 1 else add_dict_branch(tree.get(key, {}), vector[1:], value) if len(vector) == 1 \
) else add_dict_branch(tree[key] if key in tree else {},
vector[1:],
value)
return tree return tree
@ -114,21 +120,3 @@ class Singleton(type):
if cls not in cls._instances: if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs) cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls] 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

@ -21,6 +21,8 @@
# THE SOFTWARE. # THE SOFTWARE.
import codecs import codecs
import glob
import imp
import os import os
from contextlib import suppress from contextlib import suppress
@ -65,11 +67,10 @@ def ansible_template(basedir, varname, templatevars, **kwargs):
try: try:
from ansible.plugins.loader import init_plugin_loader, module_loader from ansible.plugins import module_loader
init_plugin_loader()
except ImportError: except ImportError:
from ansible.plugins.loader import module_loader from ansible.plugins.loader import init_plugin_loader, module_loader
init_plugin_loader()
LINE_NUMBER_KEY = "__line__" LINE_NUMBER_KEY = "__line__"
FILENAME_KEY = "__file__" FILENAME_KEY = "__file__"
@ -128,6 +129,24 @@ BLOCK_NAME_TO_ACTION_TYPE_MAP = {
} }
def load_plugins(directory):
result = []
fh = None
for pluginfile in glob.glob(os.path.join(directory, "[A-Za-z]*.py")):
pluginname = os.path.basename(pluginfile.replace(".py", ""))
try:
fh, filename, desc = imp.find_module(pluginname, [directory])
mod = imp.load_module(pluginname, fh, filename, desc)
obj = getattr(mod, pluginname)()
result.append(obj)
finally:
if fh:
fh.close()
return result
def tokenize(line): def tokenize(line):
tokens = line.lstrip().split(" ") tokens = line.lstrip().split(" ")
if tokens[0] == "-": if tokens[0] == "-":
@ -191,10 +210,8 @@ def template(basedir, value, variables, fail_on_undefined=False, **kwargs):
# I guess the filter doesn't like empty vars... # I guess the filter doesn't like empty vars...
with suppress(AnsibleError, ValueError): with suppress(AnsibleError, ValueError):
return ansible_template( return ansible_template(
os.path.abspath(basedir), os.path.abspath(basedir), value, variables,
value, **dict(kwargs, fail_on_undefined=fail_on_undefined)
variables,
**dict(kwargs, fail_on_undefined=fail_on_undefined),
) )
@ -217,9 +234,8 @@ def play_children(basedir, item, parent_type):
if k in delegate_map and v: if k in delegate_map and v:
v = template( v = template(
os.path.abspath(basedir), os.path.abspath(basedir),
v, v, {"playbook_dir": os.path.abspath(basedir)},
{"playbook_dir": os.path.abspath(basedir)}, fail_on_undefined=False
fail_on_undefined=False,
) )
return delegate_map[k](basedir, k, v, parent_type) return delegate_map[k](basedir, k, v, parent_type)
return [] return []
@ -250,20 +266,18 @@ def _taskshandlers_children(basedir, k, v, parent_type):
results.extend( results.extend(
_roles_children( _roles_children(
basedir, basedir,
k, k, [th["import_role"].get("name")],
[th["import_role"].get("name")],
parent_type, parent_type,
main=th["import_role"].get("tasks_from", "main"), main=th["import_role"].get("tasks_from", "main")
) )
) )
elif "include_role" in th: elif "include_role" in th:
results.extend( results.extend(
_roles_children( _roles_children(
basedir, basedir,
k, k, [th["include_role"].get("name")],
[th["include_role"].get("name")],
parent_type, parent_type,
main=th["include_role"].get("tasks_from", "main"), main=th["include_role"].get("tasks_from", "main")
) )
) )
elif "block" in th: elif "block" in th:
@ -315,7 +329,7 @@ def _rolepath(basedir, role):
path_dwim(basedir, role), path_dwim(basedir, role),
# if included from roles/[role]/meta/main.yml # if included from roles/[role]/meta/main.yml
path_dwim(basedir, os.path.join("..", "..", "..", "roles", role)), path_dwim(basedir, os.path.join("..", "..", "..", "roles", role)),
path_dwim(basedir, os.path.join("..", "..", role)), path_dwim(basedir, os.path.join("..", "..", role))
] ]
if constants.DEFAULT_ROLES_PATH: if constants.DEFAULT_ROLES_PATH:
@ -363,28 +377,30 @@ def rolename(filepath):
def _kv_to_dict(v): def _kv_to_dict(v):
(command, args, kwargs) = tokenize(v) (command, args, kwargs) = tokenize(v)
return dict(__ansible_module__=command, __ansible_arguments__=args, **kwargs) return (dict(__ansible_module__=command, __ansible_arguments__=args, **kwargs))
def normalize_task(task, filename, custom_modules=None): def normalize_task(task, filename, custom_modules=None):
"""Ensure tasks have an action key and strings are converted to python objects.""" """Ensure tasks have an action key and strings are converted to python objects."""
def _normalize(task, custom_modules):
if custom_modules is None: if custom_modules is None:
custom_modules = [] custom_modules = []
normalized = {} ansible_action_type = task.get("__ansible_action_type__", "task")
ansible_parsed_keys = ("action", "local_action", "args", "delegate_to") if "__ansible_action_type__" in task:
del (task["__ansible_action_type__"])
if is_nested_task(task): # temp. extract metadata
_extract_ansible_parsed_keys_from_task(normalized, task, ansible_parsed_keys) ansible_meta = {}
# Add dummy action for block/always/rescue statements for key in ["__line__", "__file__", "__ansible_action_meta__"]:
normalized["action"] = { default = None
"__ansible_module__": "block/always/rescue",
"__ansible_module_original__": "block/always/rescue", if key == "__ansible_action_meta__":
"__ansible_arguments__": "block/always/rescue", default = {}
}
return normalized ansible_meta[key] = task.pop(key, default)
normalized = {}
builtin = list(ansible.parsing.mod_args.BUILTIN_TASKS) builtin = list(ansible.parsing.mod_args.BUILTIN_TASKS)
builtin = list(set(builtin + custom_modules)) builtin = list(set(builtin + custom_modules))
@ -399,72 +415,52 @@ def normalize_task(task, filename, custom_modules=None):
# denormalize shell -> command conversion # denormalize shell -> command conversion
if "_uses_shell" in arguments: if "_uses_shell" in arguments:
action = "shell" action = "shell"
del arguments["_uses_shell"] del (arguments["_uses_shell"])
for k, v in list(task.items()): for (k, v) in list(task.items()):
if k in ansible_parsed_keys or k == action: if k in ("action", "local_action", "args", "delegate_to") or k == action:
# we don"t want to re-assign these values, which were # we don"t want to re-assign these values, which were
# determined by the ModuleArgsParser() above # determined by the ModuleArgsParser() above
continue continue
normalized[k] = v normalized[k] = v
# convert builtin fqn calls to short forms because most rules know only normalized["action"] = {"__ansible_module__": action}
# about short calls
normalized["action"] = {
"__ansible_module__": action.removeprefix("ansible.builtin."),
"__ansible_module_original__": action,
}
if "_raw_params" in arguments: if "_raw_params" in arguments:
normalized["action"]["__ansible_arguments__"] = ( normalized["action"]["__ansible_arguments__"] = arguments["_raw_params"].strip().split()
arguments["_raw_params"].strip().split() del (arguments["_raw_params"])
)
del arguments["_raw_params"]
else: else:
normalized["action"]["__ansible_arguments__"] = [] normalized["action"]["__ansible_arguments__"] = []
normalized["action"].update(arguments) normalized["action"].update(arguments)
return normalized
# temp. extract metadata
ansible_meta = {}
for key in ["__line__", "__file__", "__ansible_action_meta__"]:
default = None
if key == "__ansible_action_meta__":
default = {}
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 = _normalize(task, custom_modules)
normalized[FILENAME_KEY] = filename normalized[FILENAME_KEY] = filename
normalized["__ansible_action_type__"] = ansible_action_type normalized["__ansible_action_type__"] = ansible_action_type
# add back extracted metadata # add back extracted metadata
for k, v in ansible_meta.items(): for (k, v) in ansible_meta.items():
if v: if v:
normalized[k] = v normalized[k] = v
return normalized return normalized
def action_tasks(yaml, candidate): def action_tasks(yaml, file):
tasks = [] tasks = []
if candidate.filemeta in ["tasks", "handlers"]: if file["filetype"] in ["tasks", "handlers"]:
tasks = add_action_type(yaml, candidate.filemeta) tasks = add_action_type(yaml, file["filetype"])
else: else:
tasks.extend(extract_from_list(yaml, ["tasks", "handlers", "pre_tasks", "post_tasks"])) tasks.extend(extract_from_list(yaml, ["tasks", "handlers", "pre_tasks", "post_tasks"]))
# Add sub-elements of block/rescue/always to tasks list # Add sub-elements of block/rescue/always to tasks list
tasks.extend(extract_from_list(tasks, ["block", "rescue", "always"])) 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): def task_to_str(task):
@ -472,14 +468,10 @@ def task_to_str(task):
if name: if name:
return name return name
action = task.get("action") action = task.get("action")
args = " ".join( args = " ".join([
[ f"{k}={v}" for (k, v) in action.items()
f"{k}={v}"
for (k, v) in action.items()
if k not in ["__ansible_module__", "__ansible_arguments__"] if k not in ["__ansible_module__", "__ansible_arguments__"]
] ] + action.get("__ansible_arguments__"))
+ action.get("__ansible_arguments__")
)
return "{} {}".format(action["__ansible_module__"], args) return "{} {}".format(action["__ansible_module__"], args)
@ -493,10 +485,7 @@ def extract_from_list(blocks, candidates):
meta_data = dict(block) meta_data = dict(block)
for key in delete_meta_keys: for key in delete_meta_keys:
meta_data.pop(key, None) meta_data.pop(key, None)
results.extend(add_action_type(block[candidate], candidate, meta_data))
actions = add_action_type(block[candidate], candidate, meta_data)
results.extend(actions)
elif block[candidate] is not None: elif block[candidate] is not None:
raise RuntimeError( raise RuntimeError(
f"Key '{candidate}' defined, but bad value: '{block[candidate]!s}'" f"Key '{candidate}' defined, but bad value: '{block[candidate]!s}'"
@ -548,13 +537,9 @@ def parse_yaml_linenumbers(data, filename):
loader.compose_node = compose_node loader.compose_node = compose_node
loader.construct_mapping = construct_mapping loader.construct_mapping = construct_mapping
data = loader.get_single_data() or [] data = loader.get_single_data() or []
except ( except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
yaml.parser.ParserError,
yaml.scanner.ScannerError,
yaml.constructor.ConstructorError,
) as e:
raise LaterError("syntax error", e) from e raise LaterError("syntax error", e) from e
except yaml.composer.ComposerError as e: except (yaml.composer.ComposerError) as e:
e.problem = f"{e.context} {e.problem}" e.problem = f"{e.context} {e.problem}"
raise LaterError("syntax error", e) from e raise LaterError("syntax error", e) from e
return data return data
@ -585,26 +570,6 @@ def normalized_yaml(file, options):
return lines 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: class UnsafeTag:
"""Handle custom yaml unsafe tag.""" """Handle custom yaml unsafe tag."""

View File

@ -2,12 +2,13 @@
title: Documentation title: Documentation
--- ---
[![Build Status](https://ci.thegeeklab.de/api/badges/thegeeklab/ansible-later/status.svg)](https://ci.thegeeklab.de/repos/thegeeklab/ansible-later) [![Build Status](https://img.shields.io/drone/build/thegeeklab/ansible-later?logo=drone&server=https%3A%2F%2Fdrone.thegeeklab.de)](https://drone.thegeeklab.de/thegeeklab/ansible-later)
[![Docker Hub](https://img.shields.io/badge/dockerhub-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/ansible-later) [![Docker Hub](https://img.shields.io/badge/dockerhub-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/ansible-later)
[![Quay.io](https://img.shields.io/badge/quay-latest-blue.svg?logo=docker&logoColor=white)](https://quay.io/repository/thegeeklab/ansible-later) [![Quay.io](https://img.shields.io/badge/quay-latest-blue.svg?logo=docker&logoColor=white)](https://quay.io/repository/thegeeklab/ansible-later)
[![Python Version](https://img.shields.io/pypi/pyversions/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![Python Version](https://img.shields.io/pypi/pyversions/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![PyPI Status](https://img.shields.io/pypi/status/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![PyPI Status](https://img.shields.io/pypi/status/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![PyPI Release](https://img.shields.io/pypi/v/ansible-later.svg)](https://pypi.org/project/ansible-later/) [![PyPI Release](https://img.shields.io/pypi/v/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![Codecov](https://img.shields.io/codecov/c/github/thegeeklab/ansible-later)](https://codecov.io/gh/thegeeklab/ansible-later)
[![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/graphs/contributors)
[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/ansible-later) [![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) [![License: MIT](https://img.shields.io/github/license/thegeeklab/ansible-later)](https://github.com/thegeeklab/ansible-later/blob/main/LICENSE)

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 --> <!-- prettier-ignore-start -->
<!-- spellchecker-disable --> <!-- spellchecker-disable -->
{{< highlight Python "linenos=table" >}} {{< highlight Python "linenos=table" >}}
class CheckBecomeUser(RuleBase): class CheckBecomeUser(StandardBase):
rid = "ANS115" sid = "ANSIBLE0015"
description = "Become should be combined with become_user" description = "Become should be combined with become_user"
helptext = "the task has `become` enabled but `become_user` is missing" helptext = "the task has `become` enabled but `become_user` is missing"
version = "0.1"
types = ["playbook", "task", "handler"] types = ["playbook", "task", "handler"]
def check(self, candidate, settings): def check(self, candidate, settings):

View File

@ -13,4 +13,4 @@ Changes can be made in a YAML configuration file or via CLI options, which are p
Please note that YAML attributes are overwritten while YAML lists are merged in any configuration files. Please note that YAML attributes are overwritten while YAML lists are merged in any configuration files.
To simplify the linting of individual files, e.g. for debugging purposes, ansible-later ignores the `exclude_files` and `ignore_dotfiles` options when files are passed to the CLI. To simplify single file linting, e.g. for debugging purposes, ansible-later ignores the `exclude_files` and `ignore_dotfiles` options when only one file is passed to the CLI.

View File

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

View File

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

View File

@ -2,47 +2,45 @@
title: Included rules 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 | | Rule | ID | Description | Parameter |
| ----------------------------- | ------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | ----------------------------- | ----------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
| CheckYamlEmptyLines | YML101 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} | | CheckYamlEmptyLines | LINT0001 | 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} | | CheckYamlIndent | LINT0002 | 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} | | CheckYamlHyphens | LINT0003 | 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}} | | CheckYamlDocumentStart | LINT0004 | 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}} | | CheckYamlColons | LINT0005 | 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. | | | CheckYamlFile | LINT0006 | Roles file should be in YAML format. | |
| CheckYamlHasContent | YML107 | Files should contain useful content. | | | CheckYamlHasContent | LINT0007 | Files should contain useful content. | |
| CheckNativeYaml | YML108 | Use YAML format for tasks and handlers rather than key=value. | {native-yaml: {exclude: []}} | | CheckNativeYaml | LINT0008 | 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}} | | CheckYamlDocumentEnd | LINT0009 | 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 | ANSIBLE0001 | Single tasks should be separated by an empty line. | |
| CheckTaskSeparation | ANS101 | 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 |
| CheckMetaMain | ANS102 | 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. | |
| CheckUniqueNamedTask | ANS103 | 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} |
| CheckBraces | ANS104 | 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. | |
| CheckScmInSrc | ANS105 | 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\_\*]}} |
| CheckNamedTask | ANS106 | 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 |
| CheckNameFormat | ANS107 | Name of tasks and handlers must be formatted. | formats: first letter capital | | CheckCommandInsteadofModule | ANSIBLE0008 | Commands should not be used in place of modules. | |
| CheckCommandInsteadofModule | ANS108 | Commands should not be used in place of modules. | | | CheckInstallUseLatest | ANSIBLE0009 | Package managers should not install with state=latest. | |
| CheckInstallUseLatest | ANS109 | Package managers should not install with state=latest. | | | CheckShellInsteadCommand | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands. | |
| CheckShellInsteadCommand | ANS110 | Use Shell only when piping, redirecting or chaining commands. | | | CheckCommandHasChanges | ANSIBLE0011 | Commands should be idempotent and only used with some checks. | |
| CheckCommandHasChanges | ANS111 | Commands should be idempotent and only used with some checks. | | | CheckCompareToEmptyString | ANSIBLE0012 | Don't compare to "" - use `when: var` or `when: not var`. | |
| CheckCompareToEmptyString | ANS112 | 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`. | |
| CheckCompareToLiteralBool | ANS113 | 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]} |
| CheckLiteralBoolFormat | ANS114 | Literal bools should be consistent. | {literal-bools: [True, False, yes, no]} | | CheckBecomeUser | ANSIBLE0015 | Become should be combined with become_user. | |
| CheckBecomeUser | ANS115 | Become should be combined with become_user. | | | CheckFilterSeparation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | |
| CheckFilterSeparation | ANS116 | Jinja2 filters should be separated with spaces. | | | CheckCommandInsteadOfArgument | ANSIBLE0017 | Commands should not be used in place of module arguments. | |
| CheckCommandInsteadOfArgument | ANS117 | Commands should not be used in place of module arguments. | | | CheckFilePermissionMissing | ANSIBLE0018 | File permissions unset or incorrect. | |
| CheckFilePermissionMissing | ANS118 | File permissions unset or incorrect. | | | CheckFilePermissionOctal | ANSIBLE0019 | Octal file permissions must contain leading zero or be a string. | |
| CheckFilePermissionOctal | ANS119 | Octal file permissions must contain leading zero or be a string. | | | CheckGitHasVersion | ANSIBLE0020 | Git checkouts should use explicit version. | |
| CheckGitHasVersion | ANS120 | Git checkouts should use explicit version. | | | CheckMetaChangeFromDefault | ANSIBLE0021 | Roles meta/main.yml default values should be changed. | |
| CheckMetaChangeFromDefault | ANS121 | Roles meta/main.yml default values should be changed. | | | CheckWhenFormat | ANSIBLE0022 | Don't use Jinja2 in `when`. | |
| CheckWhenFormat | ANS122 | Don't use Jinja2 in `when`. | | | CheckNestedJinja | ANSIBLE0023 | Don't use nested Jinja2 pattern. | |
| CheckNestedJinja | ANS123 | Don't use nested Jinja2 pattern. | | | CheckLocalAction | ANSIBLE0024 | Don't use local_action. | |
| CheckLocalAction | ANS124 | Don't use local_action. | | | CheckRelativeRolePaths | ANSIBLE0025 | Don't use a relative path in a role. | |
| CheckRelativeRolePaths | ANS125 | Don't use a relative path in a role. | | | CheckChangedInWhen | ANSIBLE0026 | Use handlers instead of `when: changed`. | |
| CheckChangedInWhen | ANS126 | Use handlers instead of `when: changed`. | | | CheckChangedInWhen | ANSIBLE0027 | Deprecated bare variables in loops must not be used. | |
| CheckChangedInWhen | ANS127 | Deprecated bare variables in loops must not be used. | | | CheckVersion | ANSIBLE9998 | Standards version should be pinned. | |
| CheckFQCNBuiltin | ANS128 | Module actions should use full qualified collection names. | | | CheckDeprecated | ANSIBLE9999 | Deprecated features of `ansible-later` should not be used. | |
| CheckFQCNBuiltin | ANS129 | Check optimized playbook/tasks key order. | |
| CheckDeprecated | ANS999 | Deprecated features of `ansible-later` should not be used. | |

View File

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

1055
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,37 +13,40 @@ classifiers = [
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities", "Topic :: Utilities",
"Topic :: Software Development", "Topic :: Software Development",
] ]
description = "Reviews ansible playbooks, roles and inventories and suggests improvements." description = "Reviews ansible playbooks, roles and inventories and suggests improvements."
documentation = "https://ansible-later.geekdocs.de/" documentation = "https://ansible-later.geekdocs.de/"
homepage = "https://ansible-later.geekdocs.de/" homepage = "https://ansible-later.geekdocs.de/"
include = ["LICENSE"] include = [
"LICENSE",
]
keywords = ["ansible", "code", "review"] keywords = ["ansible", "code", "review"]
license = "MIT" license = "MIT"
name = "ansible-later" name = "ansible-later"
packages = [{ include = "ansiblelater" }] packages = [
{include = "ansiblelater"},
]
readme = "README.md" readme = "README.md"
repository = "https://github.com/thegeeklab/ansible-later/" repository = "https://github.com/thegeeklab/ansible-later/"
version = "0.0.0" version = "0.0.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
PyYAML = "6.0.2" PyYAML = "6.0"
ansible-core = { version = "2.14.17", optional = true } ansible = {version = "8.1.0", optional = true}
ansible = { version = "7.7.0", optional = true } ansible-core = {version = "2.15.1", optional = true}
anyconfig = "0.14.0" anyconfig = "0.13.0"
appdirs = "1.4.4" appdirs = "1.4.4"
colorama = "0.4.6" colorama = "0.4.6"
jsonschema = "4.23.0" jsonschema = "4.17.3"
nested-lookup = "0.2.25" nested-lookup = "0.2.25"
pathspec = "0.12.1" pathspec = "0.11.1"
python = "^3.9.0" python = "^3.9.0"
python-json-logger = "2.0.7" python-json-logger = "2.0.7"
toolz = "0.12.1" toolz = "0.12.0"
unidiff = "0.7.5" unidiff = "0.7.5"
yamllint = "1.35.1" yamllint = "1.32.0"
[tool.poetry.extras] [tool.poetry.extras]
ansible = ["ansible"] ansible = ["ansible"]
@ -53,11 +56,12 @@ ansible-core = ["ansible-core"]
ansible-later = "ansiblelater.__main__:main" ansible-later = "ansiblelater.__main__:main"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "0.6.7" ruff = "0.0.275"
pytest = "8.3.3" pytest = "7.4.0"
pytest-mock = "3.14.0" pytest-mock = "3.11.1"
pytest-cov = "5.0.0" pytest-cov = "4.1.0"
toml = "0.10.2" toml = "0.10.2"
yapf = "0.40.1"
[tool.poetry-dynamic-versioning] [tool.poetry-dynamic-versioning]
enable = true enable = true
@ -76,7 +80,7 @@ filterwarnings = [
omit = ["**/test/*"] omit = ["**/test/*"]
[build-system] [build-system]
build-backend = "poetry_dynamic_versioning.backend" build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
[tool.ruff] [tool.ruff]
@ -92,11 +96,6 @@ exclude = [
".eggs", ".eggs",
"env*", "env*",
] ]
line-length = 99
indent-width = 4
[tool.ruff.lint]
# Explanation of errors # Explanation of errors
# #
# D100: Missing docstring in public module # D100: Missing docstring in public module
@ -121,6 +120,7 @@ ignore = [
"UP038", "UP038",
"RUF012", "RUF012",
] ]
line-length = 99
select = [ select = [
"D", "D",
"E", "E",
@ -143,7 +143,14 @@ select = [
"RUF", "RUF",
] ]
[tool.ruff.format] [tool.ruff.flake8-quotes]
quote-style = "double" inline-quotes = "double"
indent-style = "space"
line-ending = "lf" [tool.yapf]
based_on_style = "google"
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true
indent_dictionary_value = true
allow_split_before_dict_value = false

View File

@ -6,12 +6,6 @@
"description": "Ansible base dependencies", "description": "Ansible base dependencies",
"matchPackageNames": ["ansible", "ansible-core"], "matchPackageNames": ["ansible", "ansible-core"],
"separateMinorPatch": true "separateMinorPatch": true
},
{
"matchManagers": ["woodpecker"],
"matchFileNames": [".woodpecker/test.yml"],
"matchPackageNames": ["docker.io/library/python"],
"enabled": false
} }
] ]
} }