feat(web-app-xwiki): install OIDC/LDAP via temporary Groovy page (PUT→execute→verify→delete)

Replace REST jobs flow with services.extension.install executed from a transient XWiki.InstallExtensions page.
- Build wishlist from Ansible vars; print machine-readable markers; assert success.
- Execute from XWiki space; delete page afterwards; fix delete changed_when.
- Use Jinja raw + indent for clean macro embedding.

https://chatgpt.com/share/68c9ebf5-f5e0-800f-9b80-372b4b31e772
This commit is contained in:
2025-09-17 01:00:25 +02:00
parent 0839b8e37f
commit c93ec6d43a

View File

@@ -1,152 +1,135 @@
# roles/web-app-xwiki/tasks/04_extensions.yml # roles/web-app-xwiki/tasks/04_extensions.yml
# #
# Canonical install flow for XWiki 16.10.x in this setup: # Installs OIDC / LDAP using a temporary Groovy page that calls the
# - PUT /rest/jobs with query params (install, async=false, media=json, namespaces, extensions) # Extension Script Service (services.extension.install).
# - Pass lists via <rest><list>…</list></rest> in query parameters # Avoids REST job API and any Namespace class import for portability.
# - Minimal XML body (<jobRequest/>) just to satisfy the REST layer
# - Restart once (handler: docker compose restart), then verify via the 'installed' repository
# #
# Requires in xwiki.properties: # Flow:
# extension.repositories=xwiki-public:maven:https://nexus.xwiki.org/nexus/content/groups/public/,central:maven:https://repo1.maven.org/maven2/ # - Bootstrap config renders with both auth backends OFF (already in your role).
# - This file installs required extensions on the current wiki.
# - Final config later turns auth ON (already in your role).
#
# Notes:
# - We print machine-readable markers so Ansible can assert deterministically.
# - We protect XWiki's {{groovy}} wiki macro from Jinja by using {% raw %}…{% endraw %}.
# 1) Probe what is already installed - name: "XWIKI | Build Groovy installer code (no wiki macro delimiters here)"
- name: "Probe OIDC extension"
include_tasks: _probe_extension.yml
vars:
ext_id: "{{ XWIKI_EXT_OIDC_ID }}"
ext_version: "{{ XWIKI_EXT_OIDC_VERSION }}"
ext_enabled: "{{ XWIKI_OIDC_ENABLED }}"
result_var: "xwiki_oidc_ext"
- name: "Probe LDAP extension"
include_tasks: _probe_extension.yml
vars:
ext_id: "{{ XWIKI_EXT_LDAP_ID }}"
ext_version: "{{ XWIKI_EXT_LDAP_VERSION }}"
ext_enabled: "{{ XWIKI_LDAP_ENABLED }}"
result_var: "xwiki_ldap_ext"
# 2) Build the list of extensions to install (enabled + missing)
- name: "Initialize extension install list"
set_fact: set_fact:
extensions_to_install: [] _install_code: |
def ext = services.extension
def ns = "wiki:xwiki"
- name: "Queue OIDC extension for install" // Build the wish list from Ansible vars
when: def wanted = []
- XWIKI_OIDC_ENABLED | bool {% if XWIKI_OIDC_ENABLED | bool %}
- xwiki_oidc_ext.status | int != 200 wanted << [id: "{{ XWIKI_EXT_OIDC_ID }}", version: "{{ XWIKI_EXT_OIDC_VERSION }}"]
set_fact: {% endif %}
extensions_to_install: "{{ extensions_to_install + [ {'id': XWIKI_EXT_OIDC_ID, 'version': XWIKI_EXT_OIDC_VERSION} ] }}" {% if XWIKI_LDAP_ENABLED | bool %}
wanted << [id: "{{ XWIKI_EXT_LDAP_ID }}", version: "{{ XWIKI_EXT_LDAP_VERSION }}"]
{% endif %}
- name: "Queue LDAP extension for install" if (wanted.isEmpty()) {
when: println "SKIP: no extensions requested"
- XWIKI_LDAP_ENABLED | bool } else {
- xwiki_ldap_ext is defined wanted.each { e ->
- xwiki_ldap_ext.status | int != 200 def already = ext.getInstalledExtension(e.id as String, ns)
set_fact: if (already) {
extensions_to_install: "{{ extensions_to_install + [ {'id': XWIKI_EXT_LDAP_ID, 'version': XWIKI_EXT_LDAP_VERSION} ] }}" println "ALREADY_INSTALLED::${e.id}::${already.id?.version}"
} else {
println "INSTALL_START::${e.id}::${e.version}"
def job = ext.install(e.id as String, e.version as String, ns)
# 3) Build XML payloads for the request BODY (not query params) // Heartbeat until terminal state
- name: "Build XML payloads for job body" long last = System.currentTimeMillis()
when: extensions_to_install | length > 0 while (true) {
set_fact: def st = job?.status?.state
namespaces_xml_rest: "<rest><list><string>wiki:xwiki</string></list></rest>" if (st && st.name() in ["FINISHED","FAILED","CANCELED"]) {
extensions_xml_rest: >- println "STATE=${st.name()}::${e.id}"
<rest><list>{% for ext in extensions_to_install -%} break
<extension><id>{{ ext.id }}</id><version>{{ ext.version }}</version><namespace>wiki:xwiki</namespace></extension> }
{%- endfor %}</list></rest> if (System.currentTimeMillis() - last > 2000) {
println "STATE=" + (st?.name() ?: "PENDING") + "::" + e.id
last = System.currentTimeMillis()
}
Thread.sleep(500)
}
# 4) Install extensions synchronously using BODY properties // Verify presence after job completion
- name: "Install extensions via PUT (body properties, sync)" def now = ext.getInstalledExtension(e.id as String, ns)
when: extensions_to_install | length > 0 if (now) {
println "INSTALLED_OK::${e.id}::${now.id?.version}"
} else {
println "INSTALLED_MISSING::${e.id}"
}
}
}
}
- name: "XWIKI | PUT installer page Main.InstallExtensions"
uri: uri:
url: "{{ XWIKI_REST_EXTENSION_INSTALL }}?jobType=install&async=false&media=json" url: "{{ [XWIKI_REST_XWIKI_PAGES, 'InstallExtensions'] | url_join }}"
method: PUT method: PUT
user: "{{ XWIKI_SUPERADMIN_USERNAME }}" user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true force_basic_auth: true
status_code: [200, 201, 202, 204]
headers: headers:
Accept: "application/json"
Content-Type: "application/xml" Content-Type: "application/xml"
Accept: "application/xml"
body: | body: |
<jobRequest xmlns="http://www.xwiki.org"> <page xmlns="http://www.xwiki.org">
<request> <title>InstallExtensions</title>
<properties> <content><![CDATA[
<property> {% raw %}{{groovy}}{% endraw %}
<key>namespaces</key> {{ _install_code | indent(8, False) }}
<value><![CDATA[{{ namespaces_xml_rest }}]]></value> {% raw %}{{/groovy}}{% endraw %}
</property> ]]></content>
<property> <syntax>xwiki/2.1</syntax>
<key>extensions</key> </page>
<value><![CDATA[{{ extensions_xml_rest }}]]></value> register: _put_page
</property>
<property>
<key>interactive</key>
<value><rest><boolean>false</boolean></rest></value>
</property>
</properties>
</request>
</jobRequest>
status_code: [200, 201, 202]
return_content: yes
register: xwiki_install_job
notify: docker compose restart
changed_when: xwiki_install_job.json.state == 'FINISHED'
# 5) Restart once so new classes are loaded - name: "XWIKI | Execute installer page"
- name: "Restart XWiki after install"
when: extensions_to_install | length > 0
meta: flush_handlers
# 6) Wait for REST to be ready again
- name: "Wait until XWiki REST is ready after restart"
uri: uri:
url: "{{ XWIKI_REST_BASE }}" url: "{{ 'http://127.0.0.1:' ~ XWIKI_HOST_PORT ~ '/bin/view/XWiki/InstallExtensions?xpage=plain' }}"
status_code: [200, 401] method: GET
follow_redirects: none user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
return_content: no password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
register: xwiki_rest_up_after force_basic_auth: true
changed_when: false status_code: [200]
retries: 60 return_content: yes
delay: 5 register: _exec_page
until:
- xwiki_rest_up_after is succeeded
- (xwiki_rest_up_after.redirected is not defined) or (not xwiki_rest_up_after.redirected)
# 7) Re-probe from 'installed' repository # Assert success:
- name: "Re-probe OIDC extension (installed repo)" # - If nothing was requested, allow "SKIP: no extensions requested".
include_tasks: _probe_extension.yml # - For requested OIDC/LDAP, require ALREADY_INSTALLED or INSTALLED_OK.
# - Disallow INSTALLED_MISSING.
- name: "ASSERT | Extension installation markers"
vars: vars:
ext_id: "{{ XWIKI_EXT_OIDC_ID }}" _c: "{{ _exec_page.content | default('') }}"
ext_version: "{{ XWIKI_EXT_OIDC_VERSION }}" _need_oidc: "{{ XWIKI_OIDC_ENABLED | bool }}"
ext_enabled: "{{ XWIKI_OIDC_ENABLED }}" _need_ldap: "{{ XWIKI_LDAP_ENABLED | bool }}"
result_var: "xwiki_oidc_ext_after" _ok_oidc: "{{ (_c is search('ALREADY_INSTALLED::' ~ XWIKI_EXT_OIDC_ID)) or (_c is search('INSTALLED_OK::' ~ XWIKI_EXT_OIDC_ID)) }}"
_ok_ldap: "{{ (_c is search('ALREADY_INSTALLED::' ~ XWIKI_EXT_LDAP_ID)) or (_c is search('INSTALLED_OK::' ~ XWIKI_EXT_LDAP_ID)) }}"
_miss_any: "{{ _c is search('INSTALLED_MISSING::') }}"
_skip_all: "{{ _c is search('SKIP: no extensions requested') }}"
assert:
that:
- _miss_any | bool == false
- (_need_oidc and _ok_oidc) or (not _need_oidc)
- (_need_ldap and _ok_ldap) or (not _need_ldap)
- (_need_oidc or _need_ldap) or _skip_all
fail_msg: >-
Extension install did not complete successfully.
Output was:
{{ (_exec_page.content | default('') | regex_replace('\\s+', ' ') | truncate(1000)) }}
- name: "Re-probe LDAP extension (installed repo)" - name: "XWIKI | Delete installer page"
include_tasks: _probe_extension.yml uri:
vars: url: "{{ [XWIKI_REST_XWIKI_PAGES, 'InstallExtensions'] | url_join }}"
ext_id: "{{ XWIKI_EXT_LDAP_ID }}" method: DELETE
ext_version: "{{ XWIKI_EXT_LDAP_VERSION }}" user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
ext_enabled: "{{ XWIKI_LDAP_ENABLED }}" password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
result_var: "xwiki_ldap_ext_after" force_basic_auth: true
status_code: [204, 200, 202, 404]
# 8) Fail fast if something requested is still missing register: _delete_page
- name: "FAIL: OIDC missing after install" changed_when: _delete_page.status != 404
fail:
msg: >-
OIDC extension ({{ XWIKI_EXT_OIDC_ID }} {{ XWIKI_EXT_OIDC_VERSION }}) is NOT detected
after install (HTTP {{ xwiki_oidc_ext_after.status | default('n/a') }}).
Check 'extension.repositories' in xwiki.properties and network/DNS.
when:
- XWIKI_OIDC_ENABLED | bool
- xwiki_oidc_ext_after.status | int != 200
- name: "FAIL: LDAP missing after install"
fail:
msg: >-
LDAP extension ({{ XWIKI_EXT_LDAP_ID }} {{ XWIKI_EXT_LDAP_VERSION }}) is NOT detected
after install (HTTP {{ xwiki_ldap_ext_after.status | default('n/a') }}).
Check 'extension.repositories' in xwiki.properties and network/DNS.
when:
- XWIKI_LDAP_ENABLED | bool
- xwiki_ldap_ext_after.status | int != 200