# 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'] - kc_lookup_value is defined - kc_desired is defined fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required." - name: Derive API endpoint and lookup field set_fact: kc_api: "{{ 'clients' if kc_object_kind == 'client' else 'components' }}" kc_lookup_field_eff: "{{ 'clientId' if kc_object_kind == 'client' else (kc_lookup_field | default('name')) }}" - name: Resolve object id shell: > {{ 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' register: kc_obj_id changed_when: false - name: Fail if object not found assert: that: - (kc_obj_id.stdout | trim) != '' - (kc_obj_id.stdout | trim) != 'null' fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found." - name: Read current object shell: > {{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id.stdout }} -r {{ KEYCLOAK_REALM }} --format json register: kc_cur changed_when: false # ── Build merge payload safely (avoid evaluating kc_desired[kc_merge_path] when undefined) ───────── - name: Parse current object set_fact: cur_obj: "{{ kc_cur.stdout | from_json }}" - 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)) } }}" - 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 }}" - name: Build desired object (base merge) set_fact: desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}" # 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) }} - 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) }} # 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.stdout }} -r {{ KEYCLOAK_REALM }} -f - {{ desired_obj | to_json }} JSON