A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.
Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.
Freed from the binding, the names take the emoji prefix every other task line
already has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
resolve_host_cid/wait_converged/guard_instance_dir took a run_host parameter every caller had to pass; STACK_HOST already encodes the correct host in both modes (first swarm manager, degrading to inventory_hostname in compose), so the manager-only queries now delegate to it directly and the parameter plus all caller declarations are gone. Also repairs the swarm guard include: apply/delegate_to are invalid on a HandlerTaskInclude and the previous commit carried duplicate run_once/delegate_to keys, which failed the ansible syntax-check. The keycloak handler drops its masking default; the 02b conditional collapses into the SPOT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructures the keycloak role so manager-side operations run on the
hosting node under both deployment models without per-task delegation
noise:
- tasks/02_manager_ops.yml: extracts the manager-side body into
02a_manager_ops_on_host.yml and includes it once with
`apply: delegate_to: KEYCLOAK_HOST_NODE`. Ansible's `apply:` keyword
propagates the delegate to every task in the included file so each
step (kcadm login, client / mail / RBAC / LDAP updates) runs on the
stack host without the inner files needing to know about it.
- tasks/04_login.yml: dual-path discovery. Swarm waits for the
keycloak service to converge via `container service ps`, resolves
the hosting node, and sets KEYCLOAK_HOST_NODE; compose sets it to
inventory_hostname (single-host model). Subsequent LOCAL_CID
resolution is now gated `when: DEPLOYMENT_MODE == 'swarm'` and
scoped to a single block; compose leaves KEYCLOAK_LOCAL_CID unset
and relies on vars/main.yml's
`KEYCLOAK_EXEC_CONTAINER: container exec -i {{ KEYCLOAK_LOCAL_CID
| default(KEYCLOAK_CONTAINER_ADDRESS) }}` fallback. Removes the
brittle inline shell-template else-branch that previously echoed
KEYCLOAK_CONTAINER for compose.
- tasks/05a_realm_import_run.yml: compose path runs the import
in-place via `compose down/up/run`; swarm path dispatches to a new
05b_realm_import_swarm.yml that scales the import service via
`container service update`, tails its logs, and rescales to 0. The
3 mode-gated tasks share a single `when:` wrapper to satisfy the
ansible-repeated-when-run lint.
- tasks/update/05_ldap_shared.yml (new): extracted from the previous
inline LDAP-shared block in the manager-ops handler so the
surrounding block stays under the >3-children limit. Combined
when-guard (KEYCLOAK_LDAP_ENABLED AND KEYCLOAK_LDAP_SHARED) lives
on the include.
- tasks/assert/05_ldap.yml: read-only assertion path; uses
KEYCLOAK_EXEC_KCADM (mode-agnostic).
- handlers/main.yml: drops the comment block that restated
`ignore_errors: true` on the sync-ldap-groups handler. The
ignore_errors + delegate_to + transient-failure semantics remain
intact; the prose was restating the directive, not naming a
trip-wire.
Verified internally coherent + compose-mode-safe via static audit:
include chain main -> 01_core -> 02_manager_ops -> 02a -> {04_login,
05_realm_import -> 05a (compose-branch) | 05b (swarm-branch),
update/{01_client, 02_mail_realm, 03_mail_master,
04_rbac_client_scope, 05_ldap_shared}, assert/05_ldap}. All
include_tasks targets resolve, all KEYCLOAK_* vars are set before
consumption, all kcadm/update/assert tasks are mode-agnostic.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Makefile lint integration:
* scripts/install/lint.sh: replace the prior checkmake binary install with
ensure_mbake (mbake is pip-installable, parses our Makefile correctly
whereas checkmake bailed on .ONESHELL: / .SHELLFLAGS: / ifneq).
* scripts/lint/makefile.sh: new runner — `mbake format --check Makefile`
reports formatting drift and missing .PHONY declarations.
* scripts/lint/autoformat.sh: add a two-pass `mbake format Makefile`
(mbake's formatter is not strictly idempotent on the first pass).
* Makefile: split the multi-line `.PHONY:` block into 18 single-line
`.PHONY:` declarations so mbake's parser can validate it; add the
previously-missing `install-python-dev` and `build-dependency` PHONY
targets; add `lint-makefile` aggregate-and-direct target.
* .pre-commit-config.yaml: new make-lint-makefile hook scoped to
`^Makefile$`.
* .github/workflows/lint-makefile.yml: new CI workflow (pip-install mbake
+ make lint-makefile).
* .github/workflows/ci-orchestrator.yml: wire lint-makefile into
lint-gate.needs.
Misc:
* .gitignore: add roles/importer_result.json (tooling artefact written by
galaxy-importer when it runs in roles/).
Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ensure kcadm is always authenticated before triggering LDAP group sync.
Refactor login logic into reusable KEYCLOAK_COMMAND_PERMANENT_ADMIN_LOGIN
and reuse it in login tasks and sync handler.
https://chatgpt.com/share/696d76ba-50f0-800f-8ec3-2ca9e27adcc8
The ldap group sync handler relies on rc-based retries with `until`.
Using async/poll breaks this logic because async results do not expose `rc`
unless fully polled.
This change removes async execution to ensure reliable retry and error
handling during ldap group synchronization.
https://chatgpt.com/share/696d63e9-0e3c-800f-8107-adf747e4ffe5
Ensure the ldap-roles group mapper always uses the correct parent group id
by injecting groups.path.group based on the actual Keycloak group.
Trigger LDAP → Keycloak group synchronization via a handler whenever the
mapper configuration changes, ensuring deterministic and reproducible
group imports under /roles.
https://chatgpt.com/share/696d02b4-1418-800f-8fe5-864f4e7494e8