Files
computer-playbook/plugins/action/applications_carrier.py
kevinveenbirkenbachandClaude Opus 5.5 d36e203171 fix(cache): render the applications carrier without the caller's application_id
`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>
2026-10-16 21:25:43 +02:00

91 lines
3.6 KiB
Python

"""Render the applications SPOT once and park it as the play-scoped carrier fact.
Ansible forks a fresh worker per task, so the in-process render cache of
``lookup('applications')`` is cold in every task until something renders in
a place all later workers inherit. This action renders in the worker of one
constructor task and returns the payload as a host fact; every later worker
inherits that fact at fork time and ``get_merged_applications`` serves it
instead of rendering::
- name: 🧊 Render the applications SPOT once and carry it into every task's worker
applications_carrier: {}
``tasks/stages/01_constructor.yml`` parks three of them: a block-scoped one
for the merge block, which it drops again at the block's end, one after the
``add_host`` tasks for the swarm bootstrap roles, parked only when
``DEPLOYMENT_MODE`` is ``swarm``, and the stage-wide one after the token store. ``merged_applications_cache_key`` reads ``group_names`` and the
tokens file's path and content digest, so a carrier whose groups or tokens no
longer match is rejected and the reader renders afresh::
- name: 🧊 Drop the merge-block carrier so later stages re-render
applications_carrier:
clear: true
``tasks/utils/setup/role.yml`` parks one after every role,
``tasks/utils/load/app.yml`` one after every nested application load and
``roles/sys-token-store/tasks/write.yml`` one after every persisted token;
where the key still matches they serve the carried payload without rendering.
The result is masked by the action itself: the fact is the whole rendered
tree, about 0.4 MB with every application's credentials, and printing it at
``-v`` is neither credential debugging nor readable.
Args:
clear: drop the carrier fact instead of rendering. Defaults to false.
Any other argument is an error.
"""
from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleActionFail
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.plugins.loader import lookup_loader
from utils.cache.carrier import APPLICATIONS_RENDERED_FACT, revalidate_tokens_file
class ActionModule(ActionBase):
TRANSFERS_FILES = False
def run(
self, tmp: Any = None, task_vars: dict[str, Any] | None = None
) -> dict[str, Any]:
result = super().run(tmp, task_vars)
task_vars = task_vars or {}
args = self._task.args or {}
unexpected = sorted(set(args) - {"clear"})
if unexpected:
raise AnsibleActionFail(
f"applications_carrier: unknown argument(s) {', '.join(unexpected)}; "
"the only argument is 'clear'"
)
if boolean(args.get("clear", False)):
result.update(
changed=False, ansible_facts={APPLICATIONS_RENDERED_FACT: None}
)
return result
revalidate_tokens_file(task_vars)
render_vars = dict(task_vars)
render_vars.pop("application_id", None)
previous = self._templar.available_variables
self._templar.available_variables = render_vars
try:
lookup = lookup_loader.get(
"applications", loader=self._loader, templar=self._templar
)
carrier = lookup.run([], variables=render_vars, carrier=True)[0]
finally:
self._templar.available_variables = previous
result.update(
changed=False,
ansible_facts={APPLICATIONS_RENDERED_FACT: carrier},
_ansible_no_log=True,
)
return result