# `applications` lookup 📦 Contributor-facing rules for the [applications.py](../../../../../../plugins/lookup/applications.py) lookup plugin. For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see [documentation.md](../../../../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 `applications` fact via `ansible.builtin.set_fact`, `combine`, or any custom aggregation task. - Consumers MUST NOT reintroduce `group_vars/all/05_applications.yml` or any equivalent generated dictionary file. ## Lookup Forms 🧭 | Form | Returns | |---|---| | `lookup('applications')` | Full merged mapping keyed by `application_id`. | | `lookup('applications', '')` | Merged entry for that application. Raises when missing. | | `lookup('applications', '', 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: 1. **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`, plus `roles/*/meta/secrets.yml` (post-`apply_schema()`). Variants from `roles/*/meta/variants.yml` deep-merge over the assembled per-role payload. See [layout.md](../../../../design/role/services/layout.md). 2. **Overrides** supplied through the normal Ansible variable `applications` in 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](../../../../../../plugins/action/applications_carrier.py) 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.yml` after the `networks`, `design` and `service_provider` merges, after the `add_host` placements and after the token store, so the payload renders with the same `group_names`, `groups` and 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 in `tasks/utils/load/app.yml`, and after every persisted token in `roles/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_id` removed from its variables, so the carried tree holds the play-scope result of every consumer-aligned `lookup('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', ...)` or `lookup('config', , 'users....')`, 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 through `lookup('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 `applications` fact above is unchanged. ## Adding an Application Entry ➕ You MUST add new application defaults in the owning role: 1. Create or edit the appropriate `roles//meta/.yml` files (`services.yml`, `server.yml`, `rbac.yml`, `volumes.yml`, `secrets.yml`). See [layout.md](../../../../design/role/services/layout.md) for the per-topic content. 2. The role directory name is the `application_id`. For example `roles/web-app-mailu/meta/services.yml` is exposed as `applications['web-app-mailu'].services`, `roles/web-app-mailu/meta/server.yml` as `applications['web-app-mailu'].server`, and so on. 3. 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`: ```yaml # group_vars/.yml or host_vars/.yml applications: web-app-mailu: server: domains: canonical: - mail.example.org ``` - Overrides MUST use the canonical `application_id` as 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.yml` or 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_id` is 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](../../../../../../plugins/lookup/applications_current_play.py)). ## Reference Files 📌 | File | Purpose | |---|---| | [applications.py](../../../../../../plugins/lookup/applications.py) | Runtime entry point for the `applications` lookup. | | [applications.py (cache)](../../../../../../utils/cache/applications.py) | Shared aggregation helper that builds and caches defaults (`get_application_defaults`, `get_variants`, `get_merged_applications`). | | [carrier.py](../../../../../../utils/cache/carrier.py) | The play-scoped carrier: fact name, `merged_applications_cache_key`, and the reader that serves the carried payload. | | [test_applications.py](../../../../../../tests/unit/python/plugins/lookup/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](users.md). For the per-role matrix-variant list that backs `applications` when more than one shape exists, see [variants.md](../../../../design/variants.md) (deploy-time mechanism) and [variants.md](../../role/variants.md) (file format).