summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/config.py')
-rw-r--r--src/ansiblelint/config.py266
1 files changed, 266 insertions, 0 deletions
diff --git a/src/ansiblelint/config.py b/src/ansiblelint/config.py
new file mode 100644
index 0000000..c9262c9
--- /dev/null
+++ b/src/ansiblelint/config.py
@@ -0,0 +1,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