mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
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>
231 lines
7.9 KiB
YAML
231 lines
7.9 KiB
YAML
---
|
|
- name: "🔄 Update REALM settings (merge LDAP component .config)"
|
|
keycloak_kcadm_update:
|
|
object_kind: "component"
|
|
lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}"
|
|
desired: >-
|
|
{{
|
|
(
|
|
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
|
| list | first
|
|
)
|
|
}}
|
|
merge_path: "config"
|
|
kcadm_exec: "{{ KEYCLOAK_EXEC_KCADM }}"
|
|
realm: "{{ KEYCLOAK_REALM }}"
|
|
assert_mode: "{{ MODE_ASSERT }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: ⬇️ Fetch components (JSON)
|
|
shell: >
|
|
{{ KEYCLOAK_EXEC_KCADM }} get components -r {{ KEYCLOAK_REALM }} --format json
|
|
register: kc_components
|
|
changed_when: false
|
|
|
|
- name: 🔍 Resolve LDAP component id (client-side filter; avoid unreliable --query)
|
|
set_fact:
|
|
ldap_cmp_id: >-
|
|
{{
|
|
(
|
|
kc_components.stdout | kcadm_json
|
|
| selectattr('providerType','equalto','org.keycloak.storage.UserStorageProvider')
|
|
| selectattr('providerId','equalto','ldap')
|
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
|
| map(attribute='id')
|
|
| list
|
|
| first
|
|
| default('')
|
|
) | string | trim
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: ✅ Assert LDAP component id resolved
|
|
assert:
|
|
that: ["ldap_cmp_id not in ['', 'null']"]
|
|
fail_msg: "LDAP component '{{ KEYCLOAK_LDAP_CMP_NAME }}' not found in Keycloak."
|
|
when: MODE_ASSERT_LOCAL | bool
|
|
|
|
- name: ⬇️ Pull LDAP component from dictionary (by name)
|
|
set_fact:
|
|
ldap_component_tpl: >-
|
|
{{
|
|
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
|
| list | first | default({})
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: ✅ Sanity check dictionary LDAP component
|
|
assert:
|
|
that:
|
|
- ldap_component_tpl | length > 0
|
|
- (ldap_component_tpl.subComponents | default({})) | length > 0
|
|
fail_msg: "LDAP component '{{ KEYCLOAK_LDAP_CMP_NAME }}' not found in KEYCLOAK_DICTIONARY_REALM."
|
|
when: MODE_ASSERT_LOCAL | bool
|
|
|
|
- name: 📤 Extract mapper 'ldap-roles' from template (raw)
|
|
set_fact:
|
|
desired_group_mapper_raw: >-
|
|
{{
|
|
(
|
|
ldap_component_tpl.subComponents['org.keycloak.storage.ldap.mappers.LDAPStorageMapper']
|
|
| default([])
|
|
)
|
|
| selectattr('name','equalto','ldap-roles')
|
|
| list | first | default({})
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: ✅ Ensure mapper exists in dictionary
|
|
assert:
|
|
that: ["desired_group_mapper_raw | length > 0"]
|
|
fail_msg: "'ldap-roles' mapper not found in dictionary under LDAP component."
|
|
when: MODE_ASSERT_LOCAL | bool
|
|
|
|
- name: 🔨 Build clean mapper payload (drop id/subComponents)
|
|
set_fact:
|
|
desired_group_mapper: >-
|
|
{{
|
|
desired_group_mapper_raw
|
|
| dict2items
|
|
| rejectattr('key','in',['subComponents','id'])
|
|
| list | items2dict
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: 📤 Extract desired groups.path (if any)
|
|
set_fact:
|
|
desired_groups_path: "{{ (desired_group_mapper.config['groups.path'] | default([])) | first | default('') | trim }}"
|
|
desired_groups_top: "{{ ((desired_group_mapper.config['groups.path'] | default([])) | first | default('') | trim).lstrip('/') | split('/') | first | default('')
|
|
}}"
|
|
changed_when: false
|
|
|
|
- name: ✅ Ensure groups.path.group is correct (only when groups.path is configured)
|
|
include_tasks: "{{ role_path }}/tasks/update/ldap/groups_path.yml"
|
|
when: desired_groups_top | length > 0
|
|
|
|
- name: 🔍 List 'ldap-roles' mapper ids under LDAP component (client-side filter)
|
|
set_fact:
|
|
grp_mapper_ids: >-
|
|
{{
|
|
(
|
|
kc_components.stdout | kcadm_json
|
|
| selectattr('parentId','equalto', ldap_cmp_id)
|
|
| selectattr('providerId','equalto','group-ldap-mapper')
|
|
| selectattr('providerType','equalto','org.keycloak.storage.ldap.mappers.LDAPStorageMapper')
|
|
| selectattr('name','equalto','ldap-roles')
|
|
| map(attribute='id')
|
|
| list
|
|
| map('string')
|
|
| map('trim')
|
|
| reject('equalto','')
|
|
| list
|
|
| sort
|
|
)
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: 🗑️ Delete duplicate 'ldap-roles' mappers (keep canonical first)
|
|
when: grp_mapper_ids | length > 1
|
|
shell: >
|
|
{{ KEYCLOAK_EXEC_KCADM }} delete components/{{ item }} -r {{ KEYCLOAK_REALM }}
|
|
loop: "{{ grp_mapper_ids[1:] }}"
|
|
register: del_dupes
|
|
changed_when: del_dupes.rc == 0
|
|
failed_when: del_dupes.rc != 0
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: 🔄 Refresh components (JSON) after dedupe (authoritative)
|
|
shell: >
|
|
{{ KEYCLOAK_EXEC_KCADM }} get components -r {{ KEYCLOAK_REALM }} --format json
|
|
register: kc_components_after_dedupe
|
|
changed_when: false
|
|
|
|
- name: 🔧 Set canonical mapper id (after dedupe)
|
|
set_fact:
|
|
grp_mapper_id: >-
|
|
{{
|
|
(
|
|
kc_components_after_dedupe.stdout | kcadm_json
|
|
| selectattr('parentId','equalto', ldap_cmp_id)
|
|
| selectattr('providerId','equalto','group-ldap-mapper')
|
|
| selectattr('providerType','equalto','org.keycloak.storage.ldap.mappers.LDAPStorageMapper')
|
|
| selectattr('name','equalto','ldap-roles')
|
|
| map(attribute='id')
|
|
| list
|
|
| map('string')
|
|
| map('trim')
|
|
| reject('equalto','')
|
|
| list
|
|
| sort
|
|
| first
|
|
| default('')
|
|
) | string | trim
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: "📁 Create canonical 'ldap-roles' mapper when missing"
|
|
when: (grp_mapper_id | trim) in ["", "null"]
|
|
block:
|
|
- name: 📁 Create 'ldap-roles' mapper if missing
|
|
ansible.builtin.shell: >-
|
|
printf '%s' {{ (desired_group_mapper | ldap_roles_mapper_payload(ldap_cmp_id | trim)) | quote }}
|
|
| {{ KEYCLOAK_EXEC_KCADM }} create components -r {{ KEYCLOAK_REALM }} -f -
|
|
register: create_mapper
|
|
changed_when: create_mapper.rc == 0
|
|
failed_when: create_mapper.rc != 0 and ('already exists' not in (create_mapper.stderr | lower))
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: 🔄 Refresh components (JSON) after create (authoritative)
|
|
shell: >
|
|
{{ KEYCLOAK_EXEC_KCADM }} get components -r {{ KEYCLOAK_REALM }} --format json
|
|
register: kc_components_after_create
|
|
changed_when: false
|
|
|
|
- name: 🔧 Set canonical mapper id after create
|
|
set_fact:
|
|
grp_mapper_id: >-
|
|
{{
|
|
(
|
|
kc_components_after_create.stdout | kcadm_json
|
|
| selectattr('parentId','equalto', ldap_cmp_id)
|
|
| selectattr('providerId','equalto','group-ldap-mapper')
|
|
| selectattr('providerType','equalto','org.keycloak.storage.ldap.mappers.LDAPStorageMapper')
|
|
| selectattr('name','equalto','ldap-roles')
|
|
| map(attribute='id')
|
|
| list
|
|
| map('string')
|
|
| map('trim')
|
|
| reject('equalto','')
|
|
| list
|
|
| sort
|
|
| first
|
|
| default('')
|
|
) | string | trim
|
|
}}
|
|
changed_when: false
|
|
|
|
- name: 🔄 Update 'ldap-roles' mapper config (merge only .config)
|
|
when: (grp_mapper_id | trim) not in ["", "null"]
|
|
keycloak_kcadm_update:
|
|
object_kind: "component"
|
|
lookup_field: "id"
|
|
lookup_value: "{{ grp_mapper_id | trim }}"
|
|
desired: >-
|
|
{{
|
|
desired_group_mapper
|
|
| combine({
|
|
'name': 'ldap-roles',
|
|
'parentId': (ldap_cmp_id | trim),
|
|
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
|
|
'providerId': 'group-ldap-mapper'
|
|
}, recursive=True)
|
|
}}
|
|
merge_path: "config"
|
|
kcadm_exec: "{{ KEYCLOAK_EXEC_KCADM }}"
|
|
realm: "{{ KEYCLOAK_REALM }}"
|
|
assert_mode: "{{ MODE_ASSERT }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
notify: sync-ldap-groups
|