Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.1 KiB
users lookup 👥
Contributor-facing rules for the users.py lookup plugin. For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see documentation.md.
Access Pattern 🎯
The users lookup is the ONLY supported runtime entry point for merged user configuration.
- Runtime consumers (roles, playbooks, templates, filter plugins, lookup plugins, tests) MUST read user data via
lookup('users')or a wrapper that calls it. - Consumers MUST NOT materialize a top-level
usersfact viaansible.builtin.set_fact,combine, or any custom aggregation task. - Consumers MUST NOT reintroduce
group_vars/all/04_users.ymlor any equivalent generated dictionary file.
Lookup Forms 🧭
| Form | Returns |
|---|---|
lookup('users') |
Full merged mapping keyed by the canonical user key. |
lookup('users', '<canonical_user_key>') |
Merged entry for that user. Raises when missing. |
lookup('users', '<canonical_user_key>', default_value) |
Merged entry, or default_value when the entry is missing. |
Data Sources 📚
The lookup merges exactly two sources:
- Defaults discovered from every
roles/*/meta/users.ymlin the repository (file root IS the users map; nousers:wrapper). - Overrides supplied through the normal Ansible variable
usersin inventory, group vars, host vars, or role vars.
No intermediate merged users fact exists. No other source is consulted.
Canonical User Key 🔑
- The canonical key is the mapping key used at the file root of
roles/*/meta/users.yml. - The canonical key is NOT the rendered
usernamefield inside the value. - The canonical key is the same identifier that inventory, group vars, host vars, and role vars MUST use when overriding an entry.
- You MUST NOT introduce a new keying scheme.
Adding a User Entry ➕
You MUST add new user defaults in the owning role:
-
Create or edit
roles/<role_id>/meta/users.yml. -
Add the user at the file root, keyed by its canonical key (no
users:wrapper):# roles/<role_id>/meta/users.yml administrator: username: administrator roles: - admin -
Keep generated fields (UID, GID, email, description, password) unset when the shared aggregation logic SHOULD allocate or template them. Set them explicitly only when the role requires a specific value.
The lookup discovers the entry automatically on the next run. No generator step is required.
Overriding a User Entry 🛠️
You MUST override user values through the normal Ansible variable users:
# group_vars/<group>.yml or host_vars/<host>.yml
users:
administrator:
email: admin@example.org
- Overrides MUST use the canonical user key.
- Overrides merge recursively on top of role-local defaults.
- Overrides MUST NOT be written back into
roles/*/meta/users.yml; that file is for defaults only.
What Not To Do 🚫
- You MUST NOT create
group_vars/all/04_users.ymlor any similar generated users dictionary. - You MUST NOT add a make target, script, or role task whose purpose is to generate, render, merge, or rewrite such a file.
- You MUST NOT key user overrides by
username; use the canonical key. - You MUST NOT reintroduce
default_usersordefaults_usersreferences; they do not exist anymore.
Source Of Truth 📌
| File | Purpose |
|---|---|
| users.py | Runtime entry point for the users lookup. |
| users/ (cache) | Shared aggregation helper package that builds and caches defaults, handles reserved users, UID/GID allocation, and uniqueness validation. Placeholder substitution (DOMAIN_PRIMARY / ORGANIZATION / SOFTWARE_NAME) lives in users/placeholders.py. |
| test_users.py | Unit tests covering the full-dict, single-entry, override, strict missing, and non-strict missing cases. |
For the related applications pattern see applications.md.