1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
|
"""Store configuration options as a singleton."""
from __future__ import annotations
import json
import logging
import os
import sys
import time
import urllib.request
import warnings
from argparse import Namespace
from functools import lru_cache
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from packaging.version import Version
from ansiblelint import __version__
from ansiblelint.loaders import yaml_from_file
_logger = logging.getLogger(__name__)
CACHE_DIR = (
os.path.expanduser(os.environ.get("XDG_CACHE_HOME", "~/.cache")) + "/ansible-lint"
)
DEFAULT_WARN_LIST = [
"experimental",
"jinja[spacing]", # warning until we resolve all reported false-positives
]
DEFAULT_KINDS = [
# Do not sort this list, order matters.
{"jinja2": "**/*.j2"}, # jinja2 templates are not always parsable as something else
{"jinja2": "**/*.j2.*"},
{"yaml": ".github/**/*.{yaml,yml}"}, # github workflows
{"text": "**/templates/**/*.*"}, # templates are likely not validable
{"execution-environment": "**/execution-environment.yml"},
{"ansible-lint-config": "**/.ansible-lint"},
{"ansible-lint-config": "**/.config/ansible-lint.yml"},
{"ansible-navigator-config": "**/ansible-navigator.{yaml,yml}"},
{"inventory": "**/inventory/**.{yaml,yml}"},
{"requirements": "**/meta/requirements.{yaml,yml}"}, # v1 only
# https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html
{"galaxy": "**/galaxy.yml"}, # Galaxy collection meta
{"reno": "**/releasenotes/*/*.{yaml,yml}"}, # reno release notes
{"tasks": "**/tasks/**/*.{yaml,yml}"},
{"rulebook": "**/rulebooks/*.{yml,yaml"},
{"playbook": "**/playbooks/*.{yml,yaml}"},
{"playbook": "**/*playbook*.{yml,yaml}"},
{"role": "**/roles/*/"},
{"handlers": "**/handlers/*.{yaml,yml}"},
{"vars": "**/{host_vars,group_vars,vars,defaults}/**/*.{yaml,yml}"},
{"test-meta": "**/tests/integration/targets/*/meta/main.{yaml,yml}"},
{"meta": "**/meta/main.{yaml,yml}"},
{"meta-runtime": "**/meta/runtime.{yaml,yml}"},
{"arg_specs": "**/meta/argument_specs.{yaml,yml}"}, # role argument specs
{"yaml": ".config/molecule/config.{yaml,yml}"}, # molecule global config
{
"requirements": "**/molecule/*/{collections,requirements}.{yaml,yml}"
}, # molecule old collection requirements (v1), ansible 2.8 only
{"yaml": "**/molecule/*/{base,molecule}.{yaml,yml}"}, # molecule config
{"requirements": "**/requirements.{yaml,yml}"}, # v2 and v1
{"playbook": "**/molecule/*/*.{yaml,yml}"}, # molecule playbooks
{"yaml": "**/{.ansible-lint,.yamllint}"},
{"changelog": "**/changelogs/changelog.yaml"},
{"yaml": "**/*.{yaml,yml}"},
{"yaml": "**/.*.{yaml,yml}"},
]
BASE_KINDS = [
# These assignations are only for internal use and are only inspired by
# MIME/IANA model. Their purpose is to be able to process a file based on
# it type, including generic processing of text files using the prefix.
{
"text/jinja2": "**/*.j2"
}, # jinja2 templates are not always parsable as something else
{"text/jinja2": "**/*.j2.*"},
{"text": "**/templates/**/*.*"}, # templates are likely not validable
{"text/json": "**/*.json"}, # standardized
{"text/markdown": "**/*.md"}, # https://tools.ietf.org/html/rfc7763
{"text/rst": "**/*.rst"}, # https://en.wikipedia.org/wiki/ReStructuredText
{"text/ini": "**/*.ini"},
# YAML has no official IANA assignation
{"text/yaml": "**/{.ansible-lint,.yamllint}"},
{"text/yaml": "**/*.{yaml,yml}"},
{"text/yaml": "**/.*.{yaml,yml}"},
]
PROFILES = yaml_from_file(Path(__file__).parent / "data" / "profiles.yml")
LOOP_VAR_PREFIX = "^(__|{role}_)"
options = Namespace(
cache_dir=None,
colored=True,
configured=False,
cwd=".",
display_relative_path=True,
exclude_paths=[],
format="brief",
lintables=[],
list_rules=False,
list_tags=False,
write_list=[],
parseable=False,
quiet=False,
rulesdirs=[],
skip_list=[],
tags=[],
verbosity=False,
warn_list=[],
kinds=DEFAULT_KINDS,
mock_filters=[],
mock_modules=[],
mock_roles=[],
loop_var_prefix=None,
only_builtins_allow_collections=[],
only_builtins_allow_modules=[],
var_naming_pattern=None,
offline=False,
project_dir=".", # default should be valid folder (do not use None here)
extra_vars=None,
enable_list=[],
skip_action_validation=True,
strict=False,
rules={}, # Placeholder to set and keep configurations for each rule.
profile=None,
task_name_prefix="{stem} | ",
sarif_file=None,
)
# Used to store detected tag deprecations
used_old_tags: dict[str, str] = {}
# Used to store collection list paths (with mock paths if needed)
collection_list: list[str] = []
def get_rule_config(rule_id: str) -> dict[str, Any]:
"""Get configurations for the rule ``rule_id``."""
rule_config = options.rules.get(rule_id, {})
if not isinstance(rule_config, dict): # pragma: no branch
raise RuntimeError(f"Invalid rule config for {rule_id}: {rule_config}")
return rule_config
@lru_cache
def ansible_collections_path() -> str:
"""Return collection path variable for current version of Ansible."""
# respect Ansible behavior, which is to load old name if present
for env_var in [
"ANSIBLE_COLLECTIONS_PATHS",
"ANSIBLE_COLLECTIONS_PATH",
]: # pragma: no cover
if env_var in os.environ:
return env_var
return "ANSIBLE_COLLECTIONS_PATH"
def in_venv() -> bool:
"""Determine whether Python is running from a venv."""
if hasattr(sys, "real_prefix") or os.environ.get("CONDA_EXE", None) is not None:
return True
pfx = getattr(sys, "base_prefix", sys.prefix)
return pfx != sys.prefix
def guess_install_method() -> str:
"""Guess if pip upgrade command should be used."""
package_name = "ansible-lint"
pip = ""
if in_venv():
_logger.debug("Found virtualenv, assuming `pip3 install` will work.")
pip = f"pip install --upgrade {package_name}"
elif __file__.startswith(os.path.expanduser("~/.local/lib")):
_logger.debug(
"Found --user installation, assuming `pip3 install --user` will work."
)
pip = f"pip3 install --user --upgrade {package_name}"
# By default we assume pip is not safe to be used
use_pip = False
try:
# Use pip to detect if is safe to use it to upgrade the package.
# We do imports here to for performance and reasons, and also in order
# to avoid errors if pip internals change. Also we want to avoid having
# to add pip as a dependency, so we make use of it only when present.
# trick to avoid runtime warning from inside pip: _distutils_hack/__init__.py:33: UserWarning: Setuptools is replacing distutils.
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
# pylint: disable=import-outside-toplevel
from pip._internal.metadata import get_default_environment
from pip._internal.req.req_uninstall import uninstallation_paths
dist = get_default_environment().get_distribution(package_name)
if dist:
logging.debug("Found %s dist", dist)
for _ in uninstallation_paths(dist):
use_pip = True
else:
logging.debug("Skipping %s as it is not installed.", package_name)
use_pip = False
# pylint: disable=broad-except
except Exception as exc:
# On Fedora 36, we got a AttributeError exception from pip that we want to avoid
logging.debug(exc)
use_pip = False
# We only want to recommend pip for upgrade if it looks safe to do so.
return pip if use_pip else ""
def get_version_warning() -> str:
"""Display warning if current version is outdated."""
# 0.1dev1 is special fallback version
if __version__ == "0.1.dev1": # pragma: no cover
return ""
msg = ""
data = {}
current_version = Version(__version__)
if not os.path.exists(CACHE_DIR): # pragma: no cover
os.makedirs(CACHE_DIR)
cache_file = f"{CACHE_DIR}/latest.json"
refresh = True
if os.path.exists(cache_file):
age = time.time() - os.path.getmtime(cache_file)
if age < 24 * 60 * 60:
refresh = False
with open(cache_file, encoding="utf-8") as f:
data = json.load(f)
if refresh or not data:
release_url = (
"https://api.github.com/repos/ansible/ansible-lint/releases/latest"
)
try:
with urllib.request.urlopen(release_url) as url:
data = json.load(url)
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(data, f)
except (URLError, HTTPError) as exc: # pragma: no cover
_logger.debug(
"Unable to fetch latest version from %s due to: %s", release_url, exc
)
return ""
html_url = data["html_url"]
new_version = Version(data["tag_name"][1:]) # removing v prefix from tag
if current_version > new_version:
msg = "[dim]You are using a pre-release version of ansible-lint.[/]"
elif current_version < new_version:
msg = f"""[warning]A new release of ansible-lint is available: [red]{current_version}[/] → [green][link={html_url}]{new_version}[/][/][/]"""
pip = guess_install_method()
if pip:
msg += f" Upgrade by running: [info]{pip}[/]"
return msg
|