mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-26 21:45:20 +02:00
- Improved get_service_name filter plugin (clearer suffix handling, consistent var names). - Added MODE_ASSERT flag to optionally execute validation/assertion tasks. - Fixed systemd unit handling: consistent use of %I instead of %i, correct escaping of instance names. - Unified on_failure behavior and alarm composer scripts. - Cleaned up redundant logging, handlers, and debug config. - Strengthened sys-service template resolution with assert (only active when MODE_ASSERT). - Simplified timer and suffix handling with get_service_name filter. - Hardened sensitive tasks with no_log. - Added conditional asserts across roles (Keycloak, DNS, Mailu, Discourse, etc.). These changes improve consistency, safety, and validation across the automation stack. Conversation: https://chatgpt.com/share/68a4ae28-483c-800f-b2f7-f64c7124c274
164 lines
5.6 KiB
YAML
164 lines
5.6 KiB
YAML
# Generic updater for Keycloak client/component via kcadm.
|
|
# Flow: resolve ID → read current object → merge with desired → preserve immutable fields → update via stdin.
|
|
#
|
|
# Required vars (pass via include):
|
|
# - kc_object_kind: "client" | "component"
|
|
# - kc_lookup_value: e.g., KEYCLOAK_CLIENT_ID or KEYCLOAK_LDAP_CMP_NAME
|
|
# - kc_desired: dict, e.g., KEYCLOAK_DICTIONARY_CLIENT or KEYCLOAK_DICTIONARY_LDAP
|
|
#
|
|
# Optional:
|
|
# - kc_lookup_field: override lookup field (defaults: clientId for client, name for component)
|
|
# - kc_merge_path: if set (e.g. "config"), only that subkey is merged
|
|
# - kc_force_attrs: dict to force on the final payload (merged last)
|
|
|
|
- name: Assert required vars
|
|
assert:
|
|
that:
|
|
- kc_object_kind in ['client','component','client-scope']
|
|
- kc_lookup_value is defined
|
|
- kc_desired is defined
|
|
fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
|
|
when: MODE_ASSERT | bool
|
|
|
|
- name: Derive API endpoint and lookup field
|
|
set_fact:
|
|
kc_api: >-
|
|
{{ 'clients' if kc_object_kind == 'client'
|
|
else 'components' if kc_object_kind == 'component'
|
|
else 'client-scopes' if kc_object_kind == 'client-scope'
|
|
else '' }}
|
|
kc_lookup_field_eff: >-
|
|
{{ 'clientId' if kc_object_kind == 'client'
|
|
else (kc_lookup_field | default('name')) if kc_object_kind == 'component'
|
|
else 'name' if kc_object_kind == 'client-scope'
|
|
else '' }}
|
|
|
|
- name: Resolve object id (direct when lookup_field is id)
|
|
when: kc_lookup_field_eff == 'id'
|
|
set_fact:
|
|
kc_obj_id: "{{ kc_lookup_value | string }}"
|
|
|
|
- name: Resolve object id via query
|
|
when: kc_lookup_field_eff != 'id'
|
|
shell: >
|
|
{% if kc_object_kind == 'client-scope' -%}
|
|
{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json
|
|
| jq -r '.[] | select(.{{ kc_lookup_field_eff }}=="{{ kc_lookup_value }}") | .id' | head -n1
|
|
{%- else -%}
|
|
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}
|
|
-r {{ KEYCLOAK_REALM }}
|
|
--query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
|
|
--fields id --format json | jq -r '.[0].id'
|
|
{%- endif %}
|
|
register: kc_obj_id_cmd
|
|
changed_when: false
|
|
|
|
- name: Normalize resolved id to a plain string
|
|
set_fact:
|
|
kc_obj_id: >-
|
|
{{
|
|
kc_obj_id
|
|
if kc_lookup_field_eff == 'id'
|
|
else (kc_obj_id_cmd.stdout | default('') | trim)
|
|
}}
|
|
|
|
- name: Fail if object not found
|
|
assert:
|
|
that:
|
|
- (kc_obj_id | trim) != ''
|
|
- (kc_obj_id | trim) != 'null'
|
|
fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
|
|
when: MODE_ASSERT | bool
|
|
|
|
- name: Read current object
|
|
shell: >
|
|
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id }}
|
|
-r {{ KEYCLOAK_REALM }} --format json
|
|
register: kc_cur
|
|
changed_when: false
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: Parse current object
|
|
set_fact:
|
|
cur_obj: "{{ kc_cur.stdout | from_json }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: "Safety check: providerId must match when updating a component"
|
|
when:
|
|
- kc_object_kind == 'component'
|
|
- (kc_desired.providerId is defined)
|
|
- MODE_ASSERT | bool
|
|
assert:
|
|
that:
|
|
- cur_obj.providerId == kc_desired.providerId
|
|
fail_msg: >
|
|
Refusing to update component '{{ cur_obj.name }}' (providerId={{ cur_obj.providerId }})
|
|
because desired providerId={{ kc_desired.providerId }}. Check your lookup/ID.
|
|
|
|
- name: Prepare merge payload (subpath)
|
|
when: kc_merge_path is defined and (kc_merge_path | length) > 0
|
|
set_fact:
|
|
merge_payload: "{{ { (kc_merge_path): (kc_desired[kc_merge_path] | default({}, true)) } }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: Prepare merge payload (full object)
|
|
when: kc_merge_path is not defined or (kc_merge_path | length) == 0
|
|
set_fact:
|
|
merge_payload: "{{ kc_desired }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: Build desired object (base merge)
|
|
set_fact:
|
|
desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
# Preserve immutable fields
|
|
- name: Preserve immutable fields for client
|
|
when: kc_object_kind == 'client'
|
|
set_fact:
|
|
desired_obj: >-
|
|
{{
|
|
desired_obj
|
|
| combine({
|
|
'id': cur_obj.id,
|
|
'clientId': cur_obj.clientId
|
|
}, recursive=True)
|
|
}}
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
- name: Preserve immutable fields for component
|
|
when: kc_object_kind == 'component'
|
|
set_fact:
|
|
desired_obj: >-
|
|
{{
|
|
desired_obj
|
|
| combine({
|
|
'id': cur_obj.id,
|
|
'providerId': cur_obj.providerId,
|
|
'providerType': cur_obj.providerType,
|
|
'parentId': cur_obj.parentId
|
|
}, recursive=True)
|
|
}}
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
|
|
# Preserve immutables for client-scope
|
|
- name: Preserve immutable fields for client-scope
|
|
when: kc_object_kind == 'client-scope'
|
|
set_fact:
|
|
desired_obj: "{{ desired_obj | combine({'id': cur_obj.id, 'name': cur_obj.name}, recursive=True) }}"
|
|
|
|
|
|
# Optional forced attributes (e.g., frontchannelLogout)
|
|
- name: Apply forced attributes (optional)
|
|
when: kc_force_attrs is defined
|
|
set_fact:
|
|
desired_obj: "{{ desired_obj | combine(kc_force_attrs, recursive=True) }}"
|
|
|
|
- name: Update object via stdin
|
|
shell: |
|
|
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id }} -r {{ KEYCLOAK_REALM }} -f -
|
|
{{ desired_obj | to_json }}
|
|
JSON
|
|
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
|
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|