Skip to content

Commit f3a3fe4

Browse files
authored
Refactor fqcn to recommend use of canonical names (#2604)
Closs: #2572
1 parent aa1ed71 commit f3a3fe4

4 files changed

Lines changed: 76 additions & 30 deletions

File tree

examples/playbooks/test_skip_inside_yaml.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
- skip_ansible_lint # should disable error at playbook level
66
tasks:
77
- name: Test # <-- 0 latest[hg]
8-
action: ansible.builtin.hg
9-
- name: Test latest[hg] (skipped) # noqa latest[hg]
8+
action: ansible.builtin.hg # noqa fqcn[canonical]
9+
- name: Test latest[hg] (skipped) # noqa latest[hg] fqcn[canonical]
1010
action: ansible.builtin.hg
1111

1212
- name: Test latest[git] and partial-become

examples/roles/role_for_no_same_owner/tasks/pass.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
---
22
- name: Synchronize-delegate
3-
ansible.builtin.synchronize:
3+
ansible.posix.synchronize:
44
src: dummy
55
dest: dummy
66
delegate_to: localhost
77

88
- name: Synchronize-no-same-owner
9-
ansible.builtin.synchronize:
9+
ansible.posix.synchronize:
1010
src: dummy
1111
dest: dummy
1212
owner: false

src/ansiblelint/rules/fqcn.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ This avoids ambiguity and conflicts that can cause operations to fail or produce
77

88
The `fqcn` rule has the following checks:
99

10-
- `fqcn[action]` - Checks all actions for FQCNs.
10+
- `fqcn[action]` - Use FQCN for module actions, such ...
1111
- `fqcn[action-core]` - Checks for FQCNs from the `ansible.legacy` or `ansible.builtin` collection.
12-
- `fqcn[action-redirect]` - Provides the correct FQCN to replace short actions.
12+
- `fqcn[canonical]` - You should use canonical module name ... instead of ...
1313

1414
```{note}
1515
In most cases you should declare the `ansible.builtin` collection for internal Ansible actions.
@@ -22,6 +22,21 @@ The `collections` keyword provided a temporary mechanism transitioning to Ansibl
2222
You should rewrite any content that uses the `collections:` key and avoid it where possible.
2323
```
2424

25+
## Canonical module names
26+
27+
Canonical module names are also known as **resolved module names** and they
28+
are to be preferred for most cases. Many Ansible modules have multiple aliases
29+
and redirects, as these were created over time while the content was refactored.
30+
Still, all of them do finally resolve to the same module name, but not without
31+
adding some performance overhead. As very old aliases are at some point removed,
32+
it makes to just refresh the content to make it point to the current canonical
33+
name.
34+
35+
The only exception for using a canonical name is if your code still needs to
36+
be compatible with a very old version of Ansible, one that does not know how
37+
to resolve that name. If you find yourself in such a situation, feel free to
38+
add this rule to the ignored list.
39+
2540
## Problematic Code
2641

2742
```yaml

src/ansiblelint/rules/fqcn.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""Rule definition for usage of fully qualified collection names for builtins."""
22
from __future__ import annotations
33

4+
import logging
45
import sys
56
from typing import Any
67

8+
from ansible.plugins.loader import module_loader
9+
710
from ansiblelint.errors import MatchError
811
from ansiblelint.file_utils import Lintable
912
from ansiblelint.rules import AnsibleLintRule
1013

14+
_logger = logging.getLogger(__name__)
15+
1116
builtins = [
1217
# spell-checker:disable
1318
"add_host",
@@ -92,33 +97,62 @@ class FQCNBuiltinsRule(AnsibleLintRule):
9297
)
9398
tags = ["formatting"]
9499
version_added = "v6.8.0"
100+
module_aliases: dict[str, str] = {"block/always/rescue": "block/always/rescue"}
95101

96102
def matchtask(
97103
self, task: dict[str, Any], file: Lintable | None = None
98104
) -> list[MatchError]:
99105
result = []
100106
module = task["action"]["__ansible_module_original__"]
101-
if module in builtins:
102-
result.append(
103-
self.create_matcherror(
104-
message=f"Use FQCN for builtin module actions ({module}).",
105-
details=f"Use `ansible.builtin.{module}` or `ansible.legacy.{module}` instead.",
106-
filename=file,
107-
linenumber=task["__line__"],
108-
tag="fqcn[action-core]",
109-
)
110-
)
111-
# Add here implementation for fqcn[action-redirect]
112-
elif module != "block/always/rescue" and module.count(".") < 2:
113-
result.append(
114-
self.create_matcherror(
115-
message=f"Use FQCN for module actions, such `<namespace>.<collection>.{module}`.",
116-
details=f"Action `{module}` is not FQCN.",
117-
filename=file,
118-
linenumber=task["__line__"],
119-
tag="fqcn[action]",
107+
108+
if module not in self.module_aliases:
109+
loaded_module = module_loader.find_plugin_with_context(module)
110+
target = loaded_module.resolved_fqcn
111+
self.module_aliases[module] = target
112+
if target is None:
113+
_logger.warning("Unable to resolve FQCN for module %s", module)
114+
self.module_aliases[module] = module
115+
return []
116+
if target not in self.module_aliases:
117+
self.module_aliases[target] = target
118+
119+
if module != self.module_aliases[module]:
120+
module_alias = self.module_aliases.get(module, "")
121+
if module_alias.startswith("ansible.builtin"):
122+
result.append(
123+
self.create_matcherror(
124+
message=f"Use FQCN for builtin module actions ({module}).",
125+
details=f"Use `ansible.builtin.{module}` or `ansible.legacy.{module}` instead.",
126+
filename=file,
127+
linenumber=task["__line__"],
128+
tag="fqcn[action-core]",
129+
)
120130
)
121-
)
131+
else:
132+
if module.count(".") < 2:
133+
result.append(
134+
self.create_matcherror(
135+
message=f"Use FQCN for module actions, such `{self.module_aliases[module]}`.",
136+
details=f"Action `{module}` is not FQCN.",
137+
filename=file,
138+
linenumber=task["__line__"],
139+
tag="fqcn[action]",
140+
)
141+
)
142+
# TODO(ssbarnea): Remove the c.g. and c.n. exceptions from here once
143+
# community team is flattening these.
144+
# See: https://github.com/ansible-community/community-topics/issues/147
145+
elif not module.startswith("community.general.") or module.startswith(
146+
"community.network."
147+
):
148+
result.append(
149+
self.create_matcherror(
150+
message=f"You should use canonical module name `{self.module_aliases[module]}` instead of `{module}`.",
151+
filename=file,
152+
linenumber=task["__line__"],
153+
tag="fqcn[canonical]",
154+
)
155+
)
122156
return result
123157

124158

@@ -160,10 +194,7 @@ def test_fqcn_builtin_fail(rule_runner: RunFromText) -> None:
160194
assert results[0].tag == "fqcn[action-core]"
161195
assert "Use FQCN for builtin module actions" in results[0].message
162196
assert results[1].tag == "fqcn[action]"
163-
assert (
164-
"Use FQCN for module actions, such `<namespace>.<collection>"
165-
in results[1].message
166-
)
197+
assert "Use FQCN for module actions, such" in results[1].message
167198

168199
@pytest.mark.parametrize(
169200
"rule_runner", (FQCNBuiltinsRule,), indirect=["rule_runner"]

0 commit comments

Comments
 (0)