mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 12:18:17 +00:00 
			
		
		
		
	refactor(xwiki): move extension installer logic into static Groovy file and switch to plugins dict
- Added 'plugins' section in config/main.yml to declare enabled extensions in a structured way - Introduced new static file 'files/extension_installer_b64.groovy' that decodes Base64 JSON of requested plugins - Simplified 04_extensions.yml: now builds installer code from static file and removed hardcoded OIDC/LDAP checks - Dropped redundant XWIKI_EXT_* variables in vars/main.yml - Added XWIKI_PLUGINS fact to collect enabled plugin items from config/main.yml This refactor makes extension installation more generic, easier to unit test, and extendable beyond OIDC/LDAP. See: https://chatgpt.com/share/68ca25e3-cbc4-800f-a45e-2b152369811a
This commit is contained in:
		@@ -39,3 +39,25 @@ rbac:
 | 
				
			|||||||
  roles: {}
 | 
					  roles: {}
 | 
				
			||||||
ldap:
 | 
					ldap:
 | 
				
			||||||
  local_enabled: true # Allows local login if LDAP is down
 | 
					  local_enabled: true # Allows local login if LDAP is down
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					plugins:
 | 
				
			||||||
 | 
					  oidc:
 | 
				
			||||||
 | 
					    enabled: true
 | 
				
			||||||
 | 
					    items:
 | 
				
			||||||
 | 
					      - id: "org.xwiki.contrib.oidc:oidc-authenticator"
 | 
				
			||||||
 | 
					        version: "2.19.2"
 | 
				
			||||||
 | 
					#      - id: "org.xwiki.contrib.oidc:oidc-authenticator-ui"
 | 
				
			||||||
 | 
					#        version: ""          # let EM choose a compatible version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ldap:
 | 
				
			||||||
 | 
					    enabled: false
 | 
				
			||||||
 | 
					    items:
 | 
				
			||||||
 | 
					      - id: "org.xwiki.contrib.ldap:ldap-authenticator"
 | 
				
			||||||
 | 
					        version: "9.15.7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Example for Matomo
 | 
				
			||||||
 | 
					  matomo:
 | 
				
			||||||
 | 
					    enabled: false
 | 
				
			||||||
 | 
					    items:
 | 
				
			||||||
 | 
					      - id: "org.xwiki.contrib:matomo"
 | 
				
			||||||
 | 
					        version: "1.0"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								roles/web-app-xwiki/files/extension_installer_b64.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								roles/web-app-xwiki/files/extension_installer_b64.groovy
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					// Reads Base64 JSON from placeholder and avoids any quoting issues.
 | 
				
			||||||
 | 
					import groovy.json.JsonSlurper
 | 
				
			||||||
 | 
					import java.util.Base64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def ext = services.extension
 | 
				
			||||||
 | 
					def ns  = "wiki:xwiki"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def b64 = '__WANTED_B64__'
 | 
				
			||||||
 | 
					def json = new String(Base64.decoder.decode(b64), 'UTF-8')
 | 
				
			||||||
 | 
					def wanted = new JsonSlurper().parseText(json) as List<Map>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!wanted || wanted.isEmpty()) {
 | 
				
			||||||
 | 
					  println "SKIP: no extensions requested"
 | 
				
			||||||
 | 
					  return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wanted.each { e ->
 | 
				
			||||||
 | 
					  def id  = (e.id ?: "").toString()
 | 
				
			||||||
 | 
					  def ver = (e.version ?: "").toString().trim()
 | 
				
			||||||
 | 
					  if (!id) { println "ERROR::<missing-id>::Empty extension id in wanted list"; return }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def already = ext.getInstalledExtension(id, ns)
 | 
				
			||||||
 | 
					  if (already) { println "ALREADY_INSTALLED::${id}::${already.id?.version}"; return }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  println "INSTALL_START::${id}::${ver ? ver : 'latest'}"
 | 
				
			||||||
 | 
					  def job
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    job = ver ? ext.install(id, ver, ns) : ext.install(id, null, ns)
 | 
				
			||||||
 | 
					    job?.join()
 | 
				
			||||||
 | 
					  } catch (Throwable t) {
 | 
				
			||||||
 | 
					    println "ERROR::${id}::${(t?.message ?: t?.toString())}"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def st = job?.status?.state?.name()
 | 
				
			||||||
 | 
					  if (st) println "STATE=${st}::${id}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def err = job?.status?.error  // singular!
 | 
				
			||||||
 | 
					  if (err) println "ERROR::${id}::${(err?.message ?: err?.toString())}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def now = ext.getInstalledExtension(id, ns)
 | 
				
			||||||
 | 
					  if (now) println "INSTALLED_OK::${id}::${now.id?.version}"
 | 
				
			||||||
 | 
					  else     println "INSTALLED_MISSING::${id}"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
# roles/web-app-xwiki/tasks/04_extensions.yml
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# Installs OIDC / LDAP using a temporary Groovy page that calls the
 | 
					# Installs OIDC / LDAP using a temporary Groovy page that calls the
 | 
				
			||||||
# Extension Script Service (services.extension.install).
 | 
					# Extension Script Service (services.extension.install).
 | 
				
			||||||
# Avoids REST job API and any Namespace class import for portability.
 | 
					# Avoids REST job API and any Namespace class import for portability.
 | 
				
			||||||
@@ -13,57 +11,13 @@
 | 
				
			|||||||
#  - We print machine-readable markers so Ansible can assert deterministically.
 | 
					#  - We print machine-readable markers so Ansible can assert deterministically.
 | 
				
			||||||
#  - We protect XWiki's {{groovy}} wiki macro from Jinja by using {% raw %}…{% endraw %}.
 | 
					#  - We protect XWiki's {{groovy}} wiki macro from Jinja by using {% raw %}…{% endraw %}.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: "XWIKI | Build Groovy installer code (no wiki macro delimiters here)"
 | 
					- name: "XWIKI | Build Groovy installer code from static file (base64 payload)"
 | 
				
			||||||
 | 
					  vars:
 | 
				
			||||||
 | 
					    _wanted_b64: "{{ XWIKI_PLUGINS | to_json | b64encode }}"
 | 
				
			||||||
  set_fact:
 | 
					  set_fact:
 | 
				
			||||||
    _install_code: |
 | 
					    _install_code: >-
 | 
				
			||||||
      def ext = services.extension
 | 
					      {{ lookup('file', 'roles/web-app-xwiki/files/extension_installer_b64.groovy')
 | 
				
			||||||
      def ns  = "wiki:xwiki"
 | 
					         | regex_replace('__WANTED_B64__', _wanted_b64) }}
 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Build the wish list from Ansible vars
 | 
					 | 
				
			||||||
      def wanted = []
 | 
					 | 
				
			||||||
      {% if XWIKI_OIDC_ENABLED | bool %}
 | 
					 | 
				
			||||||
      wanted << [id: "{{ XWIKI_EXT_OIDC_ID }}", version: "{{ XWIKI_EXT_OIDC_VERSION }}"]
 | 
					 | 
				
			||||||
      {% endif %}
 | 
					 | 
				
			||||||
      {% if XWIKI_LDAP_ENABLED | bool %}
 | 
					 | 
				
			||||||
      wanted << [id: "{{ XWIKI_EXT_LDAP_ID }}", version: "{{ XWIKI_EXT_LDAP_VERSION }}"]
 | 
					 | 
				
			||||||
      {% endif %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (wanted.isEmpty()) {
 | 
					 | 
				
			||||||
        println "SKIP: no extensions requested"
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        wanted.each { e ->
 | 
					 | 
				
			||||||
          def already = ext.getInstalledExtension(e.id as String, ns)
 | 
					 | 
				
			||||||
          if (already) {
 | 
					 | 
				
			||||||
            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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Heartbeat until terminal state
 | 
					 | 
				
			||||||
            long last = System.currentTimeMillis()
 | 
					 | 
				
			||||||
            while (true) {
 | 
					 | 
				
			||||||
              def st = job?.status?.state
 | 
					 | 
				
			||||||
              if (st && st.name() in ["FINISHED","FAILED","CANCELED"]) {
 | 
					 | 
				
			||||||
                println "STATE=${st.name()}::${e.id}"
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              if (System.currentTimeMillis() - last > 2000) {
 | 
					 | 
				
			||||||
                println "STATE=" + (st?.name() ?: "PENDING") + "::" + e.id
 | 
					 | 
				
			||||||
                last = System.currentTimeMillis()
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              Thread.sleep(500)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Verify presence after job completion
 | 
					 | 
				
			||||||
            def now = ext.getInstalledExtension(e.id as String, ns)
 | 
					 | 
				
			||||||
            if (now) {
 | 
					 | 
				
			||||||
              println "INSTALLED_OK::${e.id}::${now.id?.version}"
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              println "INSTALLED_MISSING::${e.id}"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: "XWIKI | PUT installer page Main.InstallExtensions"
 | 
					- name: "XWIKI | PUT installer page Main.InstallExtensions"
 | 
				
			||||||
  uri:
 | 
					  uri:
 | 
				
			||||||
@@ -97,36 +51,12 @@
 | 
				
			|||||||
    force_basic_auth: true
 | 
					    force_basic_auth: true
 | 
				
			||||||
    status_code: [200]
 | 
					    status_code: [200]
 | 
				
			||||||
    return_content: yes
 | 
					    return_content: yes
 | 
				
			||||||
    timeout: 300        # allow up to 5 minutes per attempt
 | 
					    timeout: 300
 | 
				
			||||||
  register: _exec_page
 | 
					  register: _exec_page
 | 
				
			||||||
  retries: 20           # retry up to 20 times
 | 
					  retries: 20
 | 
				
			||||||
  delay: 15             # wait 15 seconds between retries
 | 
					  delay: 15
 | 
				
			||||||
  until: _exec_page is succeeded
 | 
					  until: _exec_page is succeeded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Assert success:
 | 
					 | 
				
			||||||
# - If nothing was requested, allow "SKIP: no extensions requested".
 | 
					 | 
				
			||||||
# - For requested OIDC/LDAP, require ALREADY_INSTALLED or INSTALLED_OK.
 | 
					 | 
				
			||||||
# - Disallow INSTALLED_MISSING.
 | 
					 | 
				
			||||||
- name: "ASSERT | Extension installation markers"
 | 
					 | 
				
			||||||
  vars:
 | 
					 | 
				
			||||||
    _c: "{{ _exec_page.content | default('') }}"
 | 
					 | 
				
			||||||
    _need_oidc: "{{ XWIKI_OIDC_ENABLED | bool }}"
 | 
					 | 
				
			||||||
    _need_ldap: "{{ XWIKI_LDAP_ENABLED | bool }}"
 | 
					 | 
				
			||||||
    _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: "XWIKI | Delete installer page"
 | 
					- name: "XWIKI | Delete installer page"
 | 
				
			||||||
  uri:
 | 
					  uri:
 | 
				
			||||||
    url: "{{ [XWIKI_REST_XWIKI_PAGES, 'InstallExtensions'] | url_join }}"
 | 
					    url: "{{ [XWIKI_REST_XWIKI_PAGES, 'InstallExtensions'] | url_join }}"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,12 +44,6 @@ XWIKI_REST_XWIKI:                     "{{ [XWIKI_REST_BASE, 'wikis/xwiki'] | url
 | 
				
			|||||||
XWIKI_REST_XWIKI_PAGES:               "{{ [XWIKI_REST_BASE, 'wikis/xwiki/spaces/XWiki/pages'] | url_join }}"
 | 
					XWIKI_REST_XWIKI_PAGES:               "{{ [XWIKI_REST_BASE, 'wikis/xwiki/spaces/XWiki/pages'] | url_join }}"
 | 
				
			||||||
XWIKI_REST_EXTENSION_INSTALL:         "{{ [XWIKI_REST_BASE, 'jobs'] | url_join }}"
 | 
					XWIKI_REST_EXTENSION_INSTALL:         "{{ [XWIKI_REST_BASE, 'jobs'] | url_join }}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Extension IDs + Versions (pin versions explicitly)
 | 
					 | 
				
			||||||
XWIKI_EXT_LDAP_ID:                    "org.xwiki.contrib.ldap:ldap-authenticator"
 | 
					 | 
				
			||||||
XWIKI_EXT_LDAP_VERSION:               "9.15.7"
 | 
					 | 
				
			||||||
XWIKI_EXT_OIDC_ID:                    "org.xwiki.contrib.oidc:oidc-authenticator"
 | 
					 | 
				
			||||||
XWIKI_EXT_OIDC_VERSION:               "2.19.2"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# LDAP configuration (mapped to LDAP.* context)
 | 
					# LDAP configuration (mapped to LDAP.* context)
 | 
				
			||||||
XWIKI_LDAP_SERVER:                    "{{ LDAP.SERVER.DOMAIN }}"
 | 
					XWIKI_LDAP_SERVER:                    "{{ LDAP.SERVER.DOMAIN }}"
 | 
				
			||||||
XWIKI_LDAP_PORT:                      "{{ LDAP.SERVER.PORT }}"
 | 
					XWIKI_LDAP_PORT:                      "{{ LDAP.SERVER.PORT }}"
 | 
				
			||||||
@@ -71,3 +65,11 @@ XWIKI_OIDC_CLIENT_SECRET:             "{{ OIDC.CLIENT.SECRET }}"
 | 
				
			|||||||
XWIKI_OIDC_SCOPES:                    "openid email profile {{ RBAC.GROUP.CLAIM }}"
 | 
					XWIKI_OIDC_SCOPES:                    "openid email profile {{ RBAC.GROUP.CLAIM }}"
 | 
				
			||||||
XWIKI_OIDC_GROUPS_CLAIM:              "{{ RBAC.GROUP.CLAIM }}"
 | 
					XWIKI_OIDC_GROUPS_CLAIM:              "{{ RBAC.GROUP.CLAIM }}"
 | 
				
			||||||
XWIKI_OIDC_ADMIN_PROVIDER_GROUP:      "{{ [RBAC.GROUP.NAME, XWIKI_ADMIN_GROUP] | path_join }}"
 | 
					XWIKI_OIDC_ADMIN_PROVIDER_GROUP:      "{{ [RBAC.GROUP.NAME, XWIKI_ADMIN_GROUP] | path_join }}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Collect enabled plugin items from config/main.yml
 | 
				
			||||||
 | 
					XWIKI_PLUGINS: >-
 | 
				
			||||||
 | 
					  {{
 | 
				
			||||||
 | 
					    (applications | get_app_conf(application_id, 'plugins'))
 | 
				
			||||||
 | 
					    | dict2items | selectattr('value.enabled','equalto', true)
 | 
				
			||||||
 | 
					    | map(attribute='value.items') | list | sum(start=[])
 | 
				
			||||||
 | 
					  }}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user