1"""Store configuration options as a singleton."""
2import os
3import re
4import subprocess
5import sys
6from argparse import Namespace
7from functools import lru_cache
8from typing import Any, Dict, List, Optional, Tuple
9
10from packaging.version import Version
11
12from ansiblelint.constants import ANSIBLE_MISSING_RC
13
14DEFAULT_KINDS = [
15    # Do not sort this list, order matters.
16    {"jinja2": "**/*.j2"},  # jinja2 templates are not always parsable as something else
17    {"jinja2": "**/*.j2.*"},
18    {"inventory": "**/inventory/**.yml"},
19    {"requirements": "**/meta/requirements.yml"},  # v1 only
20    # https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html
21    {"galaxy": "**/galaxy.yml"},  # Galaxy collection meta
22    {"reno": "**/releasenotes/*/*.{yaml,yml}"},  # reno release notes
23    {"playbook": "**/playbooks/*.{yml,yaml}"},
24    {"playbook": "**/*playbook*.{yml,yaml}"},
25    {"role": "**/roles/*/"},
26    {"tasks": "**/tasks/**/*.{yaml,yml}"},
27    {"handlers": "**/handlers/*.{yaml,yml}"},
28    {"vars": "**/{host_vars,group_vars,vars,defaults}/**/*.{yaml,yml}"},
29    {"meta": "**/meta/main.{yaml,yml}"},
30    {"yaml": ".config/molecule/config.{yaml,yml}"},  # molecule global config
31    {
32        "requirements": "**/molecule/*/{collections,requirements}.{yaml,yml}"
33    },  # molecule old collection requirements (v1), ansible 2.8 only
34    {"yaml": "**/molecule/*/{base,molecule}.{yaml,yml}"},  # molecule config
35    {"requirements": "**/requirements.yml"},  # v2 and v1
36    {"playbook": "**/molecule/*/*.{yaml,yml}"},  # molecule playbooks
37    {"yaml": "**/{.ansible-lint,.yamllint}"},
38    {"yaml": "**/*.{yaml,yml}"},
39    {"yaml": "**/.*.{yaml,yml}"},
40]
41
42BASE_KINDS = [
43    # These assignations are only for internal use and are only inspired by
44    # MIME/IANA model. Their purpose is to be able to process a file based on
45    # it type, including generic processing of text files using the prefix.
46    {
47        "text/jinja2": "**/*.j2"
48    },  # jinja2 templates are not always parsable as something else
49    {"text/jinja2": "**/*.j2.*"},
50    {"text": "**/templates/**/*.*"},  # templates are likely not validable
51    {"text/json": "**/*.json"},  # standardized
52    {"text/markdown": "**/*.md"},  # https://tools.ietf.org/html/rfc7763
53    {"text/rst": "**/*.rst"},  # https://en.wikipedia.org/wiki/ReStructuredText
54    {"text/ini": "**/*.ini"},
55    # YAML has no official IANA assignation
56    {"text/yaml": "**/{.ansible-lint,.yamllint}"},
57    {"text/yaml": "**/*.{yaml,yml}"},
58    {"text/yaml": "**/.*.{yaml,yml}"},
59]
60
61
62options = Namespace(
63    cache_dir=None,
64    colored=True,
65    configured=False,
66    cwd=".",
67    display_relative_path=True,
68    exclude_paths=[],
69    lintables=[],
70    listrules=False,
71    listtags=False,
72    parseable=False,
73    parseable_severity=False,
74    quiet=False,
75    rulesdirs=[],
76    skip_list=[],
77    tags=[],
78    verbosity=False,
79    warn_list=[],
80    kinds=DEFAULT_KINDS,
81    mock_modules=[],
82    mock_roles=[],
83    loop_var_prefix=None,
84    var_naming_pattern=None,
85    offline=False,
86    project_dir=".",  # default should be valid folder (do not use None here)
87    extra_vars=None,
88    enable_list=[],
89    skip_action_validation=True,
90    rules=dict(),  # Placeholder to set and keep configurations for each rule.
91)
92
93# Used to store detected tag deprecations
94used_old_tags: Dict[str, str] = {}
95
96# Used to store collection list paths (with mock paths if needed)
97collection_list: List[str] = []
98
99
100def get_rule_config(rule_id: str) -> Dict[str, Any]:
101    """Get configurations for the rule ``rule_id``."""
102    rule_config = options.rules.get(rule_id, dict())
103    if not isinstance(rule_config, dict):
104        raise RuntimeError("Invalid rule config for %s: %s" % (rule_id, rule_config))
105    return rule_config
106
107
108@lru_cache()
109def ansible_collections_path() -> str:
110    """Return collection path variable for current version of Ansible."""
111    # respect Ansible behavior, which is to load old name if present
112    for env_var in ["ANSIBLE_COLLECTIONS_PATHS", "ANSIBLE_COLLECTIONS_PATH"]:
113        if env_var in os.environ:
114            return env_var
115
116    # https://github.com/ansible/ansible/pull/70007
117    if ansible_version() >= ansible_version("2.10.0.dev0"):
118        return "ANSIBLE_COLLECTIONS_PATH"
119    return "ANSIBLE_COLLECTIONS_PATHS"
120
121
122def parse_ansible_version(stdout: str) -> Tuple[str, Optional[str]]:
123    """Parse output of 'ansible --version'."""
124    # Ansible can produce extra output before displaying version in debug mode.
125
126    # ansible-core 2.11+: 'ansible [core 2.11.3]'
127    match = re.search(r"^ansible \[(?:core|base) ([^\]]+)\]", stdout, re.MULTILINE)
128    if match:
129        return match.group(1), None
130    # ansible-base 2.10 and Ansible 2.9: 'ansible 2.x.y'
131    match = re.search(r"^ansible ([^\s]+)", stdout, re.MULTILINE)
132    if match:
133        return match.group(1), None
134    return "", "FATAL: Unable parse ansible cli version: %s" % stdout
135
136
137@lru_cache()
138def ansible_version(version: str = "") -> Version:
139    """Return current Version object for Ansible.
140
141    If version is not mentioned, it returns current version as detected.
142    When version argument is mentioned, it return converts the version string
143    to Version object in order to make it usable in comparisons.
144    """
145    if not version:
146        proc = subprocess.run(
147            ["ansible", "--version"],
148            universal_newlines=True,
149            check=False,
150            stdout=subprocess.PIPE,
151            stderr=subprocess.PIPE,
152        )
153        if proc.returncode == 0:
154            version, error = parse_ansible_version(proc.stdout)
155            if error is not None:
156                print(error)
157                sys.exit(ANSIBLE_MISSING_RC)
158        else:
159            print(
160                "Unable to find a working copy of ansible executable.",
161                proc,
162            )
163            sys.exit(ANSIBLE_MISSING_RC)
164    return Version(version)
165
166
167if ansible_collections_path() in os.environ:
168    collection_list = os.environ[ansible_collections_path()].split(':')
169