--- # Idempotent update of Keycloak LDAP provider: # - bindDn # - bindCredential # - connectionUrl # # STRICT: Uses ONLY values from ldap.* (no computed defaults) # - ldap.dn.administrator.data # - ldap.bind_credential # - ldap.server.uri - name: "Assert required vars exist (strict: use ldap.* only, no defaults)" assert: that: - KEYCLOAK_REALM is defined - KEYCLOAK_CONTAINER is defined - KEYCLOAK_SERVER_INTERNAL_URL is defined - KEYCLOAK_MASTER_API_USER_NAME is defined - KEYCLOAK_MASTER_API_USER_PASSWORD is defined - KEYCLOAK_LDAP_CMP_NAME is defined - ldap is defined - ldap.dn.administrator is defined - ldap.dn.administrator.data is defined - ldap.bind_credential is defined - ldap.server is defined - ldap.server.uri is defined fail_msg: >- Missing required Keycloak/LDAP variables. Ensure ldap.dn.administrator.data, ldap.bind_credential, and ldap.server.uri are defined. # Build a base argv for kcadm to avoid fragile shell quoting - name: "Build kcadm argv base" set_fact: kcadm_argv_base: - docker - exec - -i - "{{ KEYCLOAK_CONTAINER }}" - /opt/keycloak/bin/kcadm.sh - name: "kcadm login (master)" no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" command: argv: "{{ kcadm_argv_base + ['config', 'credentials', '--server', KEYCLOAK_SERVER_INTERNAL_URL, '--realm', 'master', '--user', KEYCLOAK_MASTER_API_USER_NAME, '--password', KEYCLOAK_MASTER_API_USER_PASSWORD] }}" changed_when: false # Resolve the LDAP component *by name* to avoid picking the wrong one. - name: "Resolve LDAP component id by name '{{ KEYCLOAK_LDAP_CMP_NAME }}'" command: argv: "{{ kcadm_argv_base + ['get', 'components', '-r', KEYCLOAK_REALM, '--query', 'name=' ~ KEYCLOAK_LDAP_CMP_NAME, '--fields', 'id,name,providerId,config', '--format', 'json'] }}" register: kc_ldap_list changed_when: false - name: "Validate that exactly one LDAP component matched" vars: parsed: "{{ kc_ldap_list.stdout | from_json }}" assert: that: - (parsed | length) == 1 fail_msg: >- Expected exactly one LDAP component named '{{ KEYCLOAK_LDAP_CMP_NAME }}', found {{ (kc_ldap_list.stdout | from_json) | length }}. - name: "Extract current LDAP component values" no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" set_fact: kc_ldap_component_id: "{{ (kc_ldap_list.stdout | from_json)[0].id }}" kc_ldap_current_bind_dn: "{{ ((kc_ldap_list.stdout | from_json)[0].config['bindDn'] | default(['']))[0] }}" kc_ldap_current_bind_pw: "{{ ((kc_ldap_list.stdout | from_json)[0].config['bindCredential'] | default(['']))[0] }}" kc_ldap_current_connection_url: "{{ ((kc_ldap_list.stdout | from_json)[0].config['connectionUrl'] | default(['']))[0] }}" - name: "Determine if update is required" set_fact: kc_needs_update: >- {{ (kc_ldap_current_bind_dn != KEYCLOAK_LDAP_BIND_DN) or (kc_ldap_current_bind_pw != KEYCLOAK_LDAP_BIND_PW) or (kc_ldap_current_connection_url != KEYCLOAK_LDAP_URL) }} # Pass each -s as a single argv token with valid JSON (arrays), zero shell quoting issues. - name: "Update LDAP bindDn / bindCredential / connectionUrl (strict, argv)" no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" command: argv: "{{ kcadm_argv_base + ['update', 'components/' ~ kc_ldap_component_id, '-r', KEYCLOAK_REALM, '-s', 'config.bindDn=' ~ ([KEYCLOAK_LDAP_BIND_DN] | to_json), '-s', 'config.bindCredential=' ~ ([KEYCLOAK_LDAP_BIND_PW] | to_json), '-s', 'config.connectionUrl=' ~ ([KEYCLOAK_LDAP_URL] | to_json) ] }}" when: kc_needs_update | bool register: kc_bind_update async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}" poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"