`lookup('tls', ...)` aligns a cross-app domain to the consumer named by
`application_id` (utils/tls_common.py `align_domain_to_consumer`): a
clearnet-primary consumer gets the clearnet sibling of an onion-primary
target. The carrier key does not cover `application_id`, so a park that
renders inside a role hands that role's alignment to every later role
whose key matches.
The constructor and `tasks/utils/setup/role.yml` park at play scope,
where `application_id` is undefined. The re-park in
`roles/sys-token-store/tasks/write.yml` (67f6c8d7e8) and the nested-load
re-park render inside the caller: `svc-prx-openresty` at the preload
loader, `web-app-stalwart` or a provider role on the NFS host, the web
app itself under `sys-stk-front-base`. In a tor deploy that turned onion
targets into clearnet domains for onion-primary roles served from such a
park, and baked the caller's group into Nextcloud's sociallogin
`groupMapping`.
Drop `application_id` from the render's variables and from the
templar's for the duration of the render, so every park carries the
play-scope result the constructor parks have always carried. Rename the
nested re-park task so its log lines do not read as a role boundary.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7.7 KiB
applications lookup 📦
Contributor-facing rules for the applications.py lookup plugin. For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see documentation.md.
Access Pattern 🎯
The applications lookup is the ONLY supported runtime entry point for merged application configuration.
- Runtime consumers (roles, playbooks, templates, filter plugins, lookup plugins, tests) MUST read application data via
lookup('applications')or a wrapper that calls it. - Consumers MUST NOT materialize a top-level
applicationsfact viaansible.builtin.set_fact,combine, or any custom aggregation task. - Consumers MUST NOT reintroduce
group_vars/all/05_applications.ymlor any equivalent generated dictionary file.
Lookup Forms 🧭
| Form | Returns |
|---|---|
lookup('applications') |
Full merged mapping keyed by application_id. |
lookup('applications', '<application_id>') |
Merged entry for that application. Raises when missing. |
lookup('applications', '<application_id>', default_value) |
Merged entry, or default_value when the entry is missing. |
lookup('applications', carrier=True) |
{"key": [...], "applications": {...}}: the full mapping plus the cache key it is valid under. Reserved for the applications_carrier action (see below). |
Data Sources 📚
The lookup merges exactly two sources:
- Defaults discovered from each role's per-topic meta files:
roles/*/meta/server.yml,roles/*/meta/rbac.yml,roles/*/meta/services.yml,roles/*/meta/volumes.yml, plusroles/*/meta/secrets.yml(post-apply_schema()). Variants fromroles/*/meta/variants.ymldeep-merge over the assembled per-role payload. See layout.md. - Overrides supplied through the normal Ansible variable
applicationsin inventory, group vars, host vars, or role vars.
No other source is consulted.
Play-Scoped Carrier 🧊
Ansible forks a fresh worker process for every task. The in-process cache inside get_merged_applications therefore never survives from one task to the next, and every task that touches the lookup would render the full tree again (about 16 to 23 seconds per task on a CI runner, measured on the workspace jobs).
The constructor stage renders once through the applications_carrier action and parks the result together with its cache key in the host fact _INFINITO_APPLICATIONS_RENDERED (the action calls lookup('applications', carrier=True)). Every later worker inherits that fact at fork time; get_merged_applications serves it whenever the key still matches and renders only when it does not (a changed applications, users, DOMAIN_PRIMARY, SYSTEM_EMAIL_DOMAIN or group_names, or a changed path or content of the tokens file).
- The stage-wide task sits in
tasks/stages/01_constructor.ymlafter thenetworks,designandservice_providermerges, after theadd_hostplacements and after the token store, so the payload renders with the samegroup_names,groupsand user records a role would see. Moving it earlier bakes stale values into the play. - The carrier is parked again after every role in
tasks/utils/setup/role.yml, after every nested application load intasks/utils/load/app.yml, and after every persisted token inroles/sys-token-store/tasks/write.yml. The first two run on every host of the include, also on hosts the loaded role skips. A re-park whose key still matches serves the carried payload without rendering. - The action renders with
application_idremoved from its variables, so the carried tree holds the play-scope result of every consumer-alignedlookup('tls', ...), whichever role parks it. - The tokens file enters the key as the SHA-256 of its content, memoised per inode, mtime and size, so a touch keeps the key. The carrier action opens the file before it computes the key, so an NFS client keys on revalidated attributes.
- The action masks its own result: the fact is the whole rendered tree with every application's credentials, so it never reaches a log regardless of
MASK_CREDENTIALS_IN_LOGS. - User records inside the payload are the token store's state at render time. Readers of tokens captured later in the play MUST go through
lookup('users', ...)orlookup('config', <app>, 'users.<key>...'), both of which read the users cache fresh. - The carrier is private to the lookup. Consumers MUST NOT read
_INFINITO_APPLICATIONS_RENDERED; they keep going throughlookup('applications'), which is what makes the carrier transparent. - The carrier is the only materialised merged view a play MAY hold. The rule against a top-level
applicationsfact above is unchanged.
Adding an Application Entry ➕
You MUST add new application defaults in the owning role:
- Create or edit the appropriate
roles/<application_id>/meta/<topic>.ymlfiles (services.yml,server.yml,rbac.yml,volumes.yml,secrets.yml). See layout.md for the per-topic content. - The role directory name is the
application_id. For exampleroles/web-app-mailu/meta/services.ymlis exposed asapplications['web-app-mailu'].services,roles/web-app-mailu/meta/server.ymlasapplications['web-app-mailu'].server, and so on. - Keep all application-scoped defaults (services, server settings, RBAC, volumes, credentials schema) inside those role-local meta files.
The lookup discovers the entry automatically on the next run. No generator step is required.
Overriding an Application Entry 🛠️
You MUST override application values through the normal Ansible variable applications:
# group_vars/<group>.yml or host_vars/<host>.yml
applications:
web-app-mailu:
server:
domains:
canonical:
- mail.example.org
- Overrides MUST use the canonical
application_idas the key. - Overrides merge recursively on top of role-local defaults.
- Overrides MUST NOT be written back into
roles/*/meta/*.yml; those files are for defaults only.
What Not To Do 🚫
- You MUST NOT create
group_vars/all/05_applications.ymlor any similar generated applications 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 introduce an alternative key source. The
application_idis always the role directory name. - You MUST NOT filter applications inside the lookup. The lookup returns the full set; filtering by enabled or allowed apps stays a caller concern (for example applications_current_play.py).
Reference Files 📌
| File | Purpose |
|---|---|
| applications.py | Runtime entry point for the applications lookup. |
| applications.py (cache) | Shared aggregation helper that builds and caches defaults (get_application_defaults, get_variants, get_merged_applications). |
| carrier.py | The play-scoped carrier: fact name, merged_applications_cache_key, and the reader that serves the carried payload. |
| test_applications.py | Unit tests covering the full-dict, single-entry, override, strict missing, and non-strict missing cases. |
For the related users pattern see users.md.
For the per-role matrix-variant list that backs applications when more than one shape exists, see variants.md (deploy-time mechanism) and variants.md (file format).