fix(web-app): gate manager-only role bodies behind IS_STACK_HOST

Across roles/web-app-*, the typical body shape is:
  - include_role: sys-stk-full (or sys-svc-compose)   cluster prereqs
  - <host-fs writes, container exec, compose exec>    stack-host only

Under swarm-mode delegation the second group only makes sense on the
host that holds the compose stack (manager), but the role plays on
every host in groups[<role>] so workers abort on missing paths.

Two workflow sweeps (gate-manager-ops + gate-manager-ops-01core, 58
agents total, discover/plan/adversarial-verify per role) plus three
manual fixes surfaced 27 roles to refactor. Each role keeps
sys-stk-full ungated so worker-side prereqs (sys-svc-container, NFS
client) still bootstrap; the trailing manager-only block either moves
into a new tasks/02_manager_ops.yml included with
when: IS_STACK_HOST | bool (SUB_FILE pattern) or gets the gate
inlined on the one or two trailing tasks (INLINE pattern). In
compose mode IS_STACK_HOST stays true so single-host deploys are
unchanged.

Manual catches the workflow agents missed:
- web-app-fediwall: utils/once/flag.yml lives AT THE END of 01_core,
  not at the start; preserved verbatim outside the gate so workers
  still flip the run_once_<role> fact.
- web-app-xwiki: a pre-existing block(...) wraps sys-stk-full plus
  _flush_config.yml; gated only the _flush_config.yml include so
  sys-stk-full keeps running on workers.
- web-app-dashboard: 02_javascript.yml (creates /opt/compose/dashboard/
  JS dirs) ran BEFORE sys-stk-full and was ungated; added the gate.

Roles touched (INLINE): akaunting, jenkins, magento, mobilizon
Roles touched (SUB_FILE): baserow, dashboard, decidim, erpnext,
  fediwall, friendica, gitea, jitsi, keycloak, listmonk, littlejs,
  mailu, matomo, moodle, odoo, openproject, opentalk, pixelfed,
  shopware, suitecrm, taiga, xwiki, zammad
This commit is contained in:
kevinveenbirkenbach committed 2026-06-05 12:18:32 +02:00
1 parent b26bb5d8aa
commit 1da3c2eda7
50 files changed
+1439 -1359

No files matched your search

+3 -1
View File
@@ -22,4 +22,6 @@
path: "{{ AKAUNTING_SETUP_MARKER }}"
state: touch
mode: "0644"
when: akaunting_setup_enabled | bool
when:
- akaunting_setup_enabled | bool
- IS_STACK_HOST | bool
@@ -0,0 +1,67 @@
---
- name: "Run Baserow Django migrations before bootstrapping users"
ansible.builtin.command:
cmd: >-
compose exec -T
-e DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}
-e SECRET_KEY={{ BASEROW_SECRET_KEY }}
-e BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY }}
{{ BASEROW_SERVICE_NAME }}
{{ BASEROW_PYTHON }} {{ BASEROW_MANAGE_PY }} migrate --noinput
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_migrate_result
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: >
'No migrations to apply' not in
((baserow_migrate_result.stdout | default('')) ~ (baserow_migrate_result.stderr | default('')))
failed_when: baserow_migrate_result.rc != 0
# The Baserow all-in-one container can still be finishing startup while
# Ansible continues after compose up. Run migrations explicitly so Playwright
# never reaches an app with missing auth_user/core_workspace tables.
retries: 60
delay: 5
until: baserow_migrate_result.rc == 0
- name: "Restart Baserow application after applying migrations"
ansible.builtin.command: "compose restart {{ BASEROW_SERVICE_NAME }}"
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_restart_after_migrate
changed_when: baserow_restart_after_migrate.rc == 0
when: baserow_migrate_result.changed
- name: "Bootstrap Baserow Django superuser '{{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}' (idempotent)"
ansible.builtin.command:
argv:
- compose
- exec
- -T
- -e
- "DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}"
- -e
- "SECRET_KEY={{ BASEROW_SECRET_KEY }}"
- -e
- "BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY }}"
- -e
- "BOOTSTRAP_ADMIN_USERNAME={{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}"
- -e
- "BOOTSTRAP_ADMIN_EMAIL={{ BASEROW_BOOTSTRAP_ADMIN_EMAIL }}"
- -e
- "BOOTSTRAP_ADMIN_PASSWORD={{ BASEROW_BOOTSTRAP_ADMIN_PASSWORD }}"
- "{{ BASEROW_SERVICE_NAME }}"
- "{{ BASEROW_PYTHON }}"
- "{{ BASEROW_MANAGE_PY }}"
- shell
- -c
- "{{ lookup('file', role_path ~ '/files/bootstrap_admin.py') }}"
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_bootstrap_admin_result
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: "'created' in baserow_bootstrap_admin_result.stdout"
failed_when: baserow_bootstrap_admin_result.rc != 0
# Retry while Baserow's container entrypoint is still running Django
# migrations on a fresh DB. Without this the very first deploy races the
# auth_user table creation and fails with UndefinedTable.
retries: 60
delay: 5
until: baserow_bootstrap_admin_result.rc == 0
+4 -67
View File
@@ -3,71 +3,8 @@
include_role:
name: sys-stk-full
vars:
compose_handlers_flush: true # Required so that new db pw is used
compose_handlers_flush: true
- name: "Run Baserow Django migrations before bootstrapping users"
ansible.builtin.command:
cmd: >-
compose exec -T
-e DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}
-e SECRET_KEY={{ BASEROW_SECRET_KEY }}
-e BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY }}
{{ BASEROW_SERVICE_NAME }}
{{ BASEROW_PYTHON }} {{ BASEROW_MANAGE_PY }} migrate --noinput
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_migrate_result
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: >
'No migrations to apply' not in
((baserow_migrate_result.stdout | default('')) ~ (baserow_migrate_result.stderr | default('')))
failed_when: baserow_migrate_result.rc != 0
# The Baserow all-in-one container can still be finishing startup while
# Ansible continues after compose up. Run migrations explicitly so Playwright
# never reaches an app with missing auth_user/core_workspace tables.
retries: 60
delay: 5
until: baserow_migrate_result.rc == 0
- name: "Restart Baserow application after applying migrations"
ansible.builtin.command: "compose restart {{ BASEROW_SERVICE_NAME }}"
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_restart_after_migrate
changed_when: baserow_restart_after_migrate.rc == 0
when: baserow_migrate_result.changed
- name: "Bootstrap Baserow Django superuser '{{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}' (idempotent)"
ansible.builtin.command:
argv:
- compose
- exec
- -T
- -e
- "DJANGO_SETTINGS_MODULE={{ BASEROW_DJANGO_SETTINGS_MODULE }}"
- -e
- "SECRET_KEY={{ BASEROW_SECRET_KEY }}"
- -e
- "BASEROW_SECRET_KEY={{ BASEROW_SECRET_KEY }}"
- -e
- "BOOTSTRAP_ADMIN_USERNAME={{ BASEROW_BOOTSTRAP_ADMIN_USERNAME }}"
- -e
- "BOOTSTRAP_ADMIN_EMAIL={{ BASEROW_BOOTSTRAP_ADMIN_EMAIL }}"
- -e
- "BOOTSTRAP_ADMIN_PASSWORD={{ BASEROW_BOOTSTRAP_ADMIN_PASSWORD }}"
- "{{ BASEROW_SERVICE_NAME }}"
- "{{ BASEROW_PYTHON }}"
- "{{ BASEROW_MANAGE_PY }}"
- shell
- -c
- "{{ lookup('file', role_path ~ '/files/bootstrap_admin.py') }}"
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: baserow_bootstrap_admin_result
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: "'created' in baserow_bootstrap_admin_result.stdout"
failed_when: baserow_bootstrap_admin_result.rc != 0
# Retry while Baserow's container entrypoint is still running Django
# migrations on a fresh DB. Without this the very first deploy races the
# auth_user table creation and fails with UndefinedTable.
retries: 60
delay: 5
until: baserow_bootstrap_admin_result.rc == 0
- name: "Baserow manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
+4 -117
View File
@@ -3,6 +3,7 @@
- name: "Include JS routines"
include_tasks: "02_javascript.yml"
when: IS_STACK_HOST | bool
- name: "load docker, proxy for '{{ application_id }}'"
include_role:
@@ -11,120 +12,6 @@
compose_handlers_flush: false
compose_repository_pull: false
- name: "Check if host-specific config.yaml exists in {{ DASHBOARD_CONFIG_INV_PATH }}"
stat:
path: "{{ DASHBOARD_CONFIG_INV_PATH }}"
delegate_to: localhost
become: false
register: config_file
- name: Load menu categories
include_vars:
file: "menu_categories.yml"
- name: Load docker cards
set_fact:
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
- name: "Load Dashboard Brand logos"
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE) }}"
when: DASHBOARD_SIMPLEICONS_ENABLED | bool
changed_when: false
- name: Group docker cards
set_fact:
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
- name: "Render Dashboard config.yaml (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "{{ DASHBOARD_CONFIG_INV_PATH if config_file.stat.exists else DASHBOARD_CONFIG_YML_SRC_FILE }}"
dest: "{{ DASHBOARD_CONFIG_YML_DEST }}"
- name: Load rendered dashboard config before first compose start
slurp:
src: "{{ DASHBOARD_CONFIG_YML_DEST }}"
register: dashboard_rendered_config_raw
- name: Collect branding asset URLs served by the shared file service
# Reads the *actually deployed* config.yaml (which may be a
# host-specific override from DASHBOARD_CONFIG_INV_PATH, not the
# role-default template). SPOTs in vars/main.yml drive the default
# render path; this slurp covers the override case so the wait list
# always reflects what port-ui will source on startup.
set_fact:
dashboard_branding_asset_urls: >-
{{
[
(
dashboard_platform_logo_source
if dashboard_platform_logo_source.startswith(dashboard_asset_url_base)
else ''
),
(
dashboard_platform_favicon_source
if dashboard_platform_favicon_source.startswith(dashboard_asset_url_base)
else ''
),
(
dashboard_company_logo_source
if dashboard_company_logo_source.startswith(dashboard_asset_url_base)
else ''
)
]
| select('string')
| reject('equalto', '')
| unique
| list
}}
vars:
dashboard_rendered_config: "{{ dashboard_rendered_config_raw.content | b64decode | from_yaml }}"
# Dashboard branding should only depend on the asset abstraction, not the
# underlying web-svc-file host that serves /assets.
dashboard_asset_url_base: "{{ lookup('config', 'web-svc-asset', 'services.asset.url') }}"
dashboard_platform_logo_source: "{{ ((dashboard_rendered_config.get('platform') or {}).get('logo') or {}).get('source', '') }}"
dashboard_platform_favicon_source: "{{ ((dashboard_rendered_config.get('platform') or {}).get('favicon') or {}).get('source', '') }}"
dashboard_company_logo_source: "{{ ((dashboard_rendered_config.get('company') or {}).get('logo') or {}).get('source', '') }}"
changed_when: false
- name: Wait for dashboard branding assets before first compose start
uri_retry:
url: "{{ item }}"
status_code: 200
validate_certs: false
register: dashboard_branding_asset_http
retries: 60
delay: 5
until: dashboard_branding_asset_http.status == 200
loop: "{{ dashboard_branding_asset_urls }}"
loop_control:
label: "{{ item }}"
- name: "Flush compose handlers"
meta: flush_handlers
- name: "Ensure Simple Icons is reachable and refresh dashboard cards"
when: DASHBOARD_SIMPLEICONS_ENABLED | bool
block:
- name: Ensure shared Simple Icons service is loaded before refresh
include_tasks: utils/load_app.yml
vars:
load_app_id: web-svc-simpleicons
- name: Flush handlers after loading shared Simple Icons service
meta: flush_handlers
- name: Refresh dashboard cards after Simple Icons becomes reachable
include_tasks: "03_simpleicons_refresh.yml"
- name: Wait for Dashboard HTTP endpoint (required so all logos can be downloaded during initialization)
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ http_port }}/"
status_code: 200
register: dashboard_http
retries: 60
delay: 5
until: dashboard_http.status == 200
- name: "Dashboard manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,111 @@
---
- name: "Check if host-specific config.yaml exists in {{ DASHBOARD_CONFIG_INV_PATH }}"
stat:
path: "{{ DASHBOARD_CONFIG_INV_PATH }}"
delegate_to: localhost
become: false
register: config_file
- name: Load menu categories
include_vars:
file: "menu_categories.yml"
- name: Load docker cards
set_fact:
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
- name: "Load Dashboard Brand logos"
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE) }}"
when: DASHBOARD_SIMPLEICONS_ENABLED | bool
changed_when: false
- name: Group docker cards
set_fact:
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
- name: "Render Dashboard config.yaml (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "{{ DASHBOARD_CONFIG_INV_PATH if config_file.stat.exists else DASHBOARD_CONFIG_YML_SRC_FILE }}"
dest: "{{ DASHBOARD_CONFIG_YML_DEST }}"
- name: Load rendered dashboard config before first compose start
slurp:
src: "{{ DASHBOARD_CONFIG_YML_DEST }}"
register: dashboard_rendered_config_raw
- name: Collect branding asset URLs served by the shared file service
set_fact:
dashboard_branding_asset_urls: >-
{{
[
(
dashboard_platform_logo_source
if dashboard_platform_logo_source.startswith(dashboard_asset_url_base)
else ''
),
(
dashboard_platform_favicon_source
if dashboard_platform_favicon_source.startswith(dashboard_asset_url_base)
else ''
),
(
dashboard_company_logo_source
if dashboard_company_logo_source.startswith(dashboard_asset_url_base)
else ''
)
]
| select('string')
| reject('equalto', '')
| unique
| list
}}
vars:
dashboard_rendered_config: "{{ dashboard_rendered_config_raw.content | b64decode | from_yaml }}"
dashboard_asset_url_base: "{{ lookup('config', 'web-svc-asset', 'services.asset.url') }}"
dashboard_platform_logo_source: "{{ ((dashboard_rendered_config.get('platform') or {}).get('logo') or {}).get('source', '') }}"
dashboard_platform_favicon_source: "{{ ((dashboard_rendered_config.get('platform') or {}).get('favicon') or {}).get('source', '') }}"
dashboard_company_logo_source: "{{ ((dashboard_rendered_config.get('company') or {}).get('logo') or {}).get('source', '') }}"
changed_when: false
- name: Wait for dashboard branding assets before first compose start
uri_retry:
url: "{{ item }}"
status_code: 200
validate_certs: false
register: dashboard_branding_asset_http
retries: 60
delay: 5
until: dashboard_branding_asset_http.status == 200
loop: "{{ dashboard_branding_asset_urls }}"
loop_control:
label: "{{ item }}"
- name: "Flush compose handlers"
meta: flush_handlers
- name: "Ensure Simple Icons is reachable and refresh dashboard cards"
when: DASHBOARD_SIMPLEICONS_ENABLED | bool
block:
- name: Ensure shared Simple Icons service is loaded before refresh
include_tasks: utils/load_app.yml
vars:
load_app_id: web-svc-simpleicons
- name: Flush handlers after loading shared Simple Icons service
meta: flush_handlers
- name: Refresh dashboard cards after Simple Icons becomes reachable
include_tasks: "03_simpleicons_refresh.yml"
- name: Wait for Dashboard HTTP endpoint (required so all logos can be downloaded during initialization)
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ http_port }}/"
status_code: 200
register: dashboard_http
retries: 60
delay: 5
until: dashboard_http.status == 200
@@ -0,0 +1,29 @@
---
- name: "Render allowed_hosts initializer for {{ application_id }}"
ansible.builtin.template:
src: "allowed_hosts.rb.j2"
dest: "{{ lookup('container', application_id, 'directories.instance') }}allowed_hosts.rb"
mode: "0644"
notify: compose up
- name: "Deploy '{{ DECIDIM_PATCH_SCRIPT_HOST_ABS }}'"
copy:
src: "{{ DECIDIM_PATCH_SCRIPT_FILE }}"
dest: "{{ DECIDIM_PATCH_SCRIPT_HOST_ABS }}"
notify:
- compose build
- compose up
- name: "Deploy '{{ DECIDIM_PATCH_RUBY_HOST_ABS }}'"
copy:
src: "{{ DECIDIM_PATCH_RUBY_DIR }}/"
dest: "{{ DECIDIM_PATCH_RUBY_HOST_ABS }}/"
notify:
- compose build
- compose up
- name: "flush handlers of compose"
meta: flush_handlers
- name: "Run Decidim setup routines"
include_tasks: 01_setup.yml
+3 -28
View File
@@ -5,31 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Render allowed_hosts initializer for {{ application_id }}"
ansible.builtin.template:
src: "allowed_hosts.rb.j2"
dest: "{{ lookup('container', application_id, 'directories.instance') }}allowed_hosts.rb"
mode: "0644"
notify: compose up
- name: "Deploy '{{ DECIDIM_PATCH_SCRIPT_HOST_ABS }}'"
copy:
src: "{{ DECIDIM_PATCH_SCRIPT_FILE }}"
dest: "{{ DECIDIM_PATCH_SCRIPT_HOST_ABS }}"
notify:
- compose build
- compose up
- name: "Deploy '{{ DECIDIM_PATCH_RUBY_HOST_ABS }}'"
copy:
src: "{{ DECIDIM_PATCH_RUBY_DIR }}/"
dest: "{{ DECIDIM_PATCH_RUBY_HOST_ABS }}/"
notify:
- compose build
- compose up
- name: "flush handlers of compose"
meta: flush_handlers
- name: "Run Decidim setup routines"
include_tasks: 01_setup.yml
- name: "Decidim manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
+3 -25
View File
@@ -7,28 +7,6 @@
vars:
compose_handlers_flush: true
- name: "wait for {{ ERPNEXT_BACKEND_CONTAINER }} gunicorn TCP port"
ansible.builtin.shell: |
set -eo pipefail
container exec {{ ERPNEXT_BACKEND_CONTAINER }} \
bash -c 'exec 3<>/dev/tcp/localhost/{{ ERPNEXT_BACKEND_PORT }} && exec 3<&- 3>&-'
register: erpnext_backend_ready
changed_when: false
retries: 60
delay: 5
until: erpnext_backend_ready.rc == 0
- name: "bench bootstrap"
include_tasks: 02_bench_bootstrap.yml
- name: "configure OIDC"
include_tasks: 03_oidc.yml
when: ERPNEXT_OIDC_ENABLED
- name: "configure LDAP"
include_tasks: 04_ldap.yml
when: ERPNEXT_LDAP_ENABLED
- name: "configure outbound email"
include_tasks: 05_email.yml
when: ERPNEXT_EMAIL_ENABLED
- name: "ERPNext manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,26 @@
---
- name: "wait for {{ ERPNEXT_BACKEND_CONTAINER }} gunicorn TCP port"
ansible.builtin.shell: |
set -eo pipefail
container exec {{ ERPNEXT_BACKEND_CONTAINER }} \
bash -c 'exec 3<>/dev/tcp/localhost/{{ ERPNEXT_BACKEND_PORT }} && exec 3<&- 3>&-'
register: erpnext_backend_ready
changed_when: false
retries: 60
delay: 5
until: erpnext_backend_ready.rc == 0
- name: "bench bootstrap"
include_tasks: 02_bench_bootstrap.yml
- name: "configure OIDC"
include_tasks: 03_oidc.yml
when: ERPNEXT_OIDC_ENABLED
- name: "configure LDAP"
include_tasks: 04_ldap.yml
when: ERPNEXT_LDAP_ENABLED
- name: "configure outbound email"
include_tasks: 05_email.yml
when: ERPNEXT_EMAIL_ENABLED
+3 -81
View File
@@ -6,86 +6,8 @@
compose_handlers_flush: false
compose_repository_pull: false
- name: "Ensure Fediwall build dir '{{ FEDIWALL_BUILD_DIR_ABS }}' exists"
file:
path: "{{ FEDIWALL_BUILD_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Download Fediwall {{ FEDIWALL_UPSTREAM_VERSION }} release tarball"
get_url_retry:
url: "{{ FEDIWALL_UPSTREAM_TARBALL_URL }}"
dest: "{{ FEDIWALL_TARBALL_FILE }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "Ensure Fediwall upstream dir '{{ FEDIWALL_UPSTREAM_DIR_ABS }}' exists"
file:
path: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Extract Fediwall tarball into '{{ FEDIWALL_UPSTREAM_DIR_ABS }}'"
unarchive:
src: "{{ FEDIWALL_TARBALL_FILE }}"
dest: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}"
remote_src: true
extra_opts:
- --strip-components=1
notify:
- compose build
- compose up
- name: "Wipe Fediwall dist tree '{{ FEDIWALL_DIST_DIR_ABS }}' (so removed slugs disappear)"
file:
path: "{{ FEDIWALL_DIST_DIR_ABS }}"
state: absent
- name: "Recreate Fediwall dist tree '{{ FEDIWALL_DIST_DIR_ABS }}'"
file:
path: "{{ FEDIWALL_DIST_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Stage upstream Fediwall files into per-slug folder '{{ wall.slug }}'"
copy:
src: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}/"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, wall.slug ] | path_join }}/"
remote_src: true
mode: preserve
loop: "{{ FEDIWALL_WALLS }}"
loop_control:
loop_var: wall
label: "{{ wall.slug }}"
notify:
- compose build
- compose up
- name: "Render per-slug Fediwall wall-config.json for '{{ wall.slug }}'"
template:
src: "wall-config.json.j2"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, wall.slug, 'wall-config.json' ] | path_join }}"
mode: "0644"
loop: "{{ FEDIWALL_WALLS }}"
loop_control:
loop_var: wall
label: "{{ wall.slug }}"
notify:
- compose build
- compose up
- name: "Render Fediwall root link list at '{{ FEDIWALL_DIST_DIR_ABS }}/index.html'"
template:
src: "index.html.j2"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, 'index.html' ] | path_join }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "flush compose for '{{ application_id }}'"
meta: flush_handlers
- name: "Fediwall manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
- include_tasks: utils/once/flag.yml
@@ -0,0 +1,82 @@
---
- name: "Ensure Fediwall build dir '{{ FEDIWALL_BUILD_DIR_ABS }}' exists"
file:
path: "{{ FEDIWALL_BUILD_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Download Fediwall {{ FEDIWALL_UPSTREAM_VERSION }} release tarball"
get_url_retry:
url: "{{ FEDIWALL_UPSTREAM_TARBALL_URL }}"
dest: "{{ FEDIWALL_TARBALL_FILE }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "Ensure Fediwall upstream dir '{{ FEDIWALL_UPSTREAM_DIR_ABS }}' exists"
file:
path: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Extract Fediwall tarball into '{{ FEDIWALL_UPSTREAM_DIR_ABS }}'"
unarchive:
src: "{{ FEDIWALL_TARBALL_FILE }}"
dest: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}"
remote_src: true
extra_opts:
- --strip-components=1
notify:
- compose build
- compose up
- name: "Wipe Fediwall dist tree '{{ FEDIWALL_DIST_DIR_ABS }}' (so removed slugs disappear)"
file:
path: "{{ FEDIWALL_DIST_DIR_ABS }}"
state: absent
- name: "Recreate Fediwall dist tree '{{ FEDIWALL_DIST_DIR_ABS }}'"
file:
path: "{{ FEDIWALL_DIST_DIR_ABS }}"
state: directory
mode: "0755"
- name: "Stage upstream Fediwall files into per-slug folder '{{ wall.slug }}'"
copy:
src: "{{ FEDIWALL_UPSTREAM_DIR_ABS }}/"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, wall.slug ] | path_join }}/"
remote_src: true
mode: preserve
loop: "{{ FEDIWALL_WALLS }}"
loop_control:
loop_var: wall
label: "{{ wall.slug }}"
notify:
- compose build
- compose up
- name: "Render per-slug Fediwall wall-config.json for '{{ wall.slug }}'"
template:
src: "wall-config.json.j2"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, wall.slug, 'wall-config.json' ] | path_join }}"
mode: "0644"
loop: "{{ FEDIWALL_WALLS }}"
loop_control:
loop_var: wall
label: "{{ wall.slug }}"
notify:
- compose build
- compose up
- name: "Render Fediwall root link list at '{{ FEDIWALL_DIST_DIR_ABS }}/index.html'"
template:
src: "index.html.j2"
dest: "{{ [ FEDIWALL_DIST_DIR_ABS, 'index.html' ] | path_join }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "flush compose for '{{ application_id }}'"
meta: flush_handlers
+3 -33
View File
@@ -7,36 +7,6 @@
vars:
compose_handlers_flush: false
- name: Integrate LDAP
include_tasks: 02_ldap.yml
when: FRIENDICA_LDAP_ENABLED | bool
# General
- name: "flush handlers to ensure that '{{ FRIENDICA_CONTAINER }}' is up before executing container tasks"
meta: flush_handlers
- name: "Remove stale ldapauth.config.php leftover when LDAP is disabled"
command: >
container exec --user root {{ FRIENDICA_CONTAINER }}
rm -f {{ FRIENDICA_LDAP_CONFIG_DOCKER }}
register: friendica_ldap_config_cleanup
changed_when: friendica_ldap_config_cleanup.rc == 0
when: not (FRIENDICA_LDAP_ENABLED | bool)
- name: Check if Friendica local.config.php exists
command: >
container exec --user {{ FRIENDICA_USER }} {{ FRIENDICA_CONTAINER }} test -f {{ FRIENDICA_CONFIG_FILE }}
register: friendica_config_exists
changed_when: false
failed_when: false
- name: Patch Friendica local.config.php with updated DB credentials
include_tasks: 03_patch_config.yml
when: friendica_config_exists.rc == 0
- name: Add Friendica Add Ons
include_tasks: 04_addons.yml
- name: Bootstrap Friendica administrator user (only when no LDAP provider is wired to provision them)
include_tasks: 06_user_bootstrap.yml
when: not (FRIENDICA_LDAP_ENABLED | bool)
- name: "Friendica manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,33 @@
---
- name: Integrate LDAP
include_tasks: 02_ldap.yml
when: FRIENDICA_LDAP_ENABLED | bool
- name: "flush handlers to ensure that '{{ FRIENDICA_CONTAINER }}' is up before executing container tasks"
meta: flush_handlers
- name: "Remove stale ldapauth.config.php leftover when LDAP is disabled"
command: >
container exec --user root {{ FRIENDICA_CONTAINER }}
rm -f {{ FRIENDICA_LDAP_CONFIG_DOCKER }}
register: friendica_ldap_config_cleanup
changed_when: friendica_ldap_config_cleanup.rc == 0
when: not (FRIENDICA_LDAP_ENABLED | bool)
- name: Check if Friendica local.config.php exists
command: >
container exec --user {{ FRIENDICA_USER }} {{ FRIENDICA_CONTAINER }} test -f {{ FRIENDICA_CONFIG_FILE }}
register: friendica_config_exists
changed_when: false
failed_when: false
- name: Patch Friendica local.config.php with updated DB credentials
include_tasks: 03_patch_config.yml
when: friendica_config_exists.rc == 0
- name: Add Friendica Add Ons
include_tasks: 04_addons.yml
- name: Bootstrap Friendica administrator user (only when no LDAP provider is wired to provision them)
include_tasks: 06_user_bootstrap.yml
when: not (FRIENDICA_LDAP_ENABLED | bool)
+3 -64
View File
@@ -7,67 +7,6 @@
vars:
compose_handlers_flush: true
- name: Wait for Gitea HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}"
delay: 5
timeout: 300
- name: Patch Gitea database settings in app.ini
include_tasks: 02_database.yml
- name: "Run DB migrations inside Gitea container"
shell: |
container exec -i --user {{ GITEA_USER }} {{ GITEA_CONTAINER }} \
/app/gitea/gitea migrate
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: migrate
changed_when: "'migrations completed' in migrate.stdout"
- name: "Create initial admin user"
shell: |
container exec -i --user {{ GITEA_USER }} {{ GITEA_CONTAINER }} \
/app/gitea/gitea admin user create \
--admin \
--username "{{ lookup('users', 'administrator').username }}" \
--password {{ lookup('users', 'administrator').password | quote }} \
--email "{{ lookup('users', 'administrator').email }}" \
-c {{ GITEA_CONFIG }}
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: create_admin
changed_when: "'has been successfully created' in create_admin.stdout"
failed_when: create_admin.rc != 0 and 'user already exists' not in create_admin.stderr
- name: "Wait until Gitea setup and migrations are ready"
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}/api/v1/version"
method: GET
status_code: 200
return_content: false
register: gitea_ready
until: gitea_ready.status == 200
retries: 20
delay: 5
when: (GITEA_OIDC_ENABLED | bool) or (GITEA_LDAP_ENABLED | bool)
- name: Execute Setup Routines
include_tasks: 03_setup.yml
- name: Execute Cleanup Routines
include_tasks: 04_cleanup.yml
when: MODE_CLEANUP | bool
- name: Include DNS role to register Gitea domain(s)
include_role:
name: sys-dns-cloudflare-records
vars:
cloudflare_records:
- zone: "{{ lookup('domain',application_id) | to_zone }}"
type: A
name: "{{ lookup('domain',application_id) }}"
content: "{{ networks.internet.ip4 }}"
proxied: false # Necessary for SSH port
when: DNS_PROVIDER == 'cloudflare'
- name: "Gitea manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,65 @@
---
- name: Wait for Gitea HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}"
delay: 5
timeout: 300
- name: Patch Gitea database settings in app.ini
include_tasks: 02_database.yml
- name: "Run DB migrations inside Gitea container"
shell: |
container exec -i --user {{ GITEA_USER }} {{ GITEA_CONTAINER }} \
/app/gitea/gitea migrate
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: migrate
changed_when: "'migrations completed' in migrate.stdout"
- name: "Create initial admin user"
shell: |
container exec -i --user {{ GITEA_USER }} {{ GITEA_CONTAINER }} \
/app/gitea/gitea admin user create \
--admin \
--username "{{ lookup('users', 'administrator').username }}" \
--password {{ lookup('users', 'administrator').password | quote }} \
--email "{{ lookup('users', 'administrator').email }}" \
-c {{ GITEA_CONFIG }}
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: create_admin
changed_when: "'has been successfully created' in create_admin.stdout"
failed_when: create_admin.rc != 0 and 'user already exists' not in create_admin.stderr
- name: "Wait until Gitea setup and migrations are ready"
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}/api/v1/version"
method: GET
status_code: 200
return_content: false
register: gitea_ready
until: gitea_ready.status == 200
retries: 20
delay: 5
when: (GITEA_OIDC_ENABLED | bool) or (GITEA_LDAP_ENABLED | bool)
- name: Execute Setup Routines
include_tasks: 03_setup.yml
- name: Execute Cleanup Routines
include_tasks: 04_cleanup.yml
when: MODE_CLEANUP | bool
- name: Include DNS role to register Gitea domain(s)
include_role:
name: sys-dns-cloudflare-records
vars:
cloudflare_records:
- zone: "{{ lookup('domain',application_id) | to_zone }}"
type: A
name: "{{ lookup('domain',application_id) }}"
content: "{{ networks.internet.ip4 }}"
proxied: false
when: DNS_PROVIDER == 'cloudflare'
+2
View File
@@ -14,6 +14,7 @@
mode: "0644"
notify:
- compose up
when: IS_STACK_HOST | bool
# `sys-svc-compose` auto-discovers and lays down the Dockerfile, but
# the Dockerfile `COPY` lines reference sidecar artefacts (plugins.txt,
@@ -32,3 +33,4 @@
notify:
- compose build
- compose up
when: IS_STACK_HOST | bool
+3 -20
View File
@@ -7,23 +7,6 @@
vars:
compose_handlers_flush: false
- name: "ensure prosody plugin + config override dirs exist (templates need them before containers come up)"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ JITSI_PROSODY_PLUGINS_HOST_PATH }}"
- "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}"
- "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}/conf.d"
- name: "stage prosody LDAP authentication override (mod_auth_ldap2)"
ansible.builtin.template:
src: prosody-ldap.cfg.lua.j2
dest: "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}/conf.d/ldap.cfg.lua"
mode: "0644"
notify: compose up
when: JITSI_LDAP_ENABLED
- name: "flush handlers so Jitsi containers start with rendered env"
meta: flush_handlers
- name: "Jitsi manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,21 @@
---
- name: "ensure prosody plugin + config override dirs exist (templates need them before containers come up)"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ JITSI_PROSODY_PLUGINS_HOST_PATH }}"
- "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}"
- "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}/conf.d"
- name: "stage prosody LDAP authentication override (mod_auth_ldap2)"
ansible.builtin.template:
src: prosody-ldap.cfg.lua.j2
dest: "{{ JITSI_PROSODY_CONFIG_HOST_PATH }}/conf.d/ldap.cfg.lua"
mode: "0644"
notify: compose up
when: JITSI_LDAP_ENABLED
- name: "flush handlers so Jitsi containers start with rendered env"
meta: flush_handlers
+3 -60
View File
@@ -14,63 +14,6 @@
vars:
compose_handlers_flush: true
- name: "Load Login routines for '{{ application_id }}'"
include_tasks: 04_login.yml
- name: "Load one-shot realm import routines for '{{ application_id }}'"
include_tasks: 05_realm_import.yml
- name: "Load Client Update routines for '{{ application_id }}'"
include_tasks: update/01_client.yml
- name: "Load Mail Update routines for '{{ application_id }} - {{ KEYCLOAK_REALM }}'"
include_tasks: update/02_mail_realm.yml
- name: "Load Mail Update routines for '{{ application_id }} - master'"
include_tasks: update/03_mail_master.yml
- name: Ensure jq is installed
become: true
package:
name: jq
state: present
- name: "Load RBAC Update routines for '{{ application_id }}'"
include_tasks: update/04_rbac_client_scope.yml
- block:
- name: "Load LDAP Update routines for '{{ application_id }}'"
include_tasks: update/05_ldap.yml
- name: Assert Keycloak LDAP configuration
include_tasks: assert/05_ldap.yml
when: MODE_ASSERT | bool
- name: "Load per-app group-ldap-mapper routines for '{{ application_id }}'"
include_tasks: update/05c_per_app_mappers.yml
when:
- KEYCLOAK_LDAP_ENABLED | bool
- KEYCLOAK_LDAP_SHARED | bool
- name: "Remove LDAP federation routines for '{{ application_id }}' when LDAP is disabled"
include_tasks: update/05_ldap_disabled.yml
when: not KEYCLOAK_LDAP_ENABLED | bool
- name: "Load reCAPTCHA Update routines for '{{ application_id }}'"
include_tasks: update/06_recaptcha.yml
when: KEYCLOAK_RECAPTCHA_ENABLED | bool
- name: "Load UserProfile (declarative) Update routines for '{{ application_id }}'"
include_tasks: update/07_userprofile.yml
- name: "Load Realm User provisioning routines for '{{ application_id }}'"
include_tasks: update/08_users_realm.yml
when: not KEYCLOAK_LDAP_ENABLED | bool
- name: "Load Users Update routines for '{{ application_id }}' (emailVerified)"
include_tasks: update/08_users_email_verified.yml
when: KEYCLOAK_USER_EMAILS_VERIFIED | bool
- name: "Flush handlers before leaving Keycloak scope"
meta: flush_handlers
- name: "Keycloak manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,61 @@
---
- name: "Load Login routines for '{{ application_id }}'"
include_tasks: 04_login.yml
- name: "Load one-shot realm import routines for '{{ application_id }}'"
include_tasks: 05_realm_import.yml
- name: "Load Client Update routines for '{{ application_id }}'"
include_tasks: update/01_client.yml
- name: "Load Mail Update routines for '{{ application_id }} - {{ KEYCLOAK_REALM }}'"
include_tasks: update/02_mail_realm.yml
- name: "Load Mail Update routines for '{{ application_id }} - master'"
include_tasks: update/03_mail_master.yml
- name: Ensure jq is installed
become: true
package:
name: jq
state: present
- name: "Load RBAC Update routines for '{{ application_id }}'"
include_tasks: update/04_rbac_client_scope.yml
- block:
- name: "Load LDAP Update routines for '{{ application_id }}'"
include_tasks: update/05_ldap.yml
- name: Assert Keycloak LDAP configuration
include_tasks: assert/05_ldap.yml
when: MODE_ASSERT | bool
- name: "Load per-app group-ldap-mapper routines for '{{ application_id }}'"
include_tasks: update/05c_per_app_mappers.yml
when:
- KEYCLOAK_LDAP_ENABLED | bool
- KEYCLOAK_LDAP_SHARED | bool
- name: "Remove LDAP federation routines for '{{ application_id }}' when LDAP is disabled"
include_tasks: update/05_ldap_disabled.yml
when: not KEYCLOAK_LDAP_ENABLED | bool
- name: "Load reCAPTCHA Update routines for '{{ application_id }}'"
include_tasks: update/06_recaptcha.yml
when: KEYCLOAK_RECAPTCHA_ENABLED | bool
- name: "Load UserProfile (declarative) Update routines for '{{ application_id }}'"
include_tasks: update/07_userprofile.yml
- name: "Load Realm User provisioning routines for '{{ application_id }}'"
include_tasks: update/08_users_realm.yml
when: not KEYCLOAK_LDAP_ENABLED | bool
- name: "Load Users Update routines for '{{ application_id }}' (emailVerified)"
include_tasks: update/08_users_email_verified.yml
when: KEYCLOAK_USER_EMAILS_VERIFIED | bool
- name: "Flush handlers before leaving Keycloak scope"
meta: flush_handlers
@@ -0,0 +1,49 @@
---
- name: add config.toml
template:
src: "config.toml.j2"
dest: "{{ LISTMONK_CONFIG_HOST }}"
notify: compose up
- meta: flush_handlers
- name: Include database setup routine for Listmonk
include_tasks: "01_database.yml"
- name: Build OIDC settings JSON
set_fact:
oidc_settings_json: >-
{{ {
"enabled": True,
"client_id": OIDC.CLIENT.ID,
"provider_url": OIDC.CLIENT.ISSUER_URL,
"client_secret": OIDC.CLIENT.SECRET
} | to_json }}
- name: Update administrator email and password login in Listmonk (as superuser)
shell: |
{{ LISTMONK_EXEC_DATABASE }} -v ON_ERROR_STOP=1 << 'EOSQL'
UPDATE users
SET email = '{{ lookup('users', 'administrator').email }}',
password_login = {{ 'false' if lookup('config',application_id, 'services.sso.enabled') else 'true' }}
WHERE username = 'administrator';
EOSQL
args:
executable: /bin/bash
- name: Apply all Listmonk settings
shell: |
{{ LISTMONK_EXEC_DATABASE }} -v ON_ERROR_STOP=1 << 'EOSQL'
UPDATE settings
SET value = '{{ item.value }}'::jsonb
WHERE key = '{{ item.key }}';
EOSQL
args:
executable: /bin/bash
loop: "{{ LISTMONK_SETTINGS }}"
loop_control:
label: "{{ item.key }}"
when: (item.when | default(true)) | bool
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
+3 -48
View File
@@ -11,51 +11,6 @@
""
{% endif %}
- name: add config.toml
template:
src: "config.toml.j2"
dest: "{{ LISTMONK_CONFIG_HOST }}"
notify: compose up
- meta: flush_handlers
- name: Include database setup routine for Listmonk
include_tasks: "01_database.yml"
- name: Build OIDC settings JSON
set_fact:
oidc_settings_json: >-
{{ {
"enabled": True,
"client_id": OIDC.CLIENT.ID,
"provider_url": OIDC.CLIENT.ISSUER_URL,
"client_secret": OIDC.CLIENT.SECRET
} | to_json }}
- name: Update administrator email and password login in Listmonk (as superuser)
shell: |
{{ LISTMONK_EXEC_DATABASE }} -v ON_ERROR_STOP=1 << 'EOSQL'
UPDATE users
SET email = '{{ lookup('users', 'administrator').email }}',
password_login = {{ 'false' if lookup('config',application_id, 'services.sso.enabled') else 'true' }}
WHERE username = 'administrator';
EOSQL
args:
executable: /bin/bash
- name: Apply all Listmonk settings
shell: |
{{ LISTMONK_EXEC_DATABASE }} -v ON_ERROR_STOP=1 << 'EOSQL'
UPDATE settings
SET value = '{{ item.value }}'::jsonb
WHERE key = '{{ item.key }}';
EOSQL
args:
executable: /bin/bash
loop: "{{ LISTMONK_SETTINGS }}"
loop_control:
label: "{{ item.key }}"
when: (item.when | default(true)) | bool
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
- name: "Listmonk manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
+3 -26
View File
@@ -8,29 +8,6 @@
compose_repository_pull: true
compose_repository_ref: "{{ LITTLEJS_REF }}"
- name: "Load LittleJS example metadata"
include_vars:
file: "examples.yml"
- name: "Render LittleJS index.html"
template:
src: "html/index.html.j2"
dest: "{{ LITTLEJS_INDEX_HOST_ABS }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "Render LittleJS run.html for shorts"
template:
src: "html/run.html.j2"
dest: "{{ LITTLEJS_RUN_HOST_ABS }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "flush compose for '{{ application_id }}'"
meta: flush_handlers
- include_tasks: utils/once/flag.yml
- name: "LittleJS manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,27 @@
---
- name: "Load LittleJS example metadata"
include_vars:
file: "examples.yml"
- name: "Render LittleJS index.html"
template:
src: "html/index.html.j2"
dest: "{{ LITTLEJS_INDEX_HOST_ABS }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "Render LittleJS run.html for shorts"
template:
src: "html/run.html.j2"
dest: "{{ LITTLEJS_RUN_HOST_ABS }}"
mode: "0644"
notify:
- compose build
- compose up
- name: "flush compose for '{{ application_id }}'"
meta: flush_handlers
- include_tasks: utils/once/flag.yml
+2
View File
@@ -21,6 +21,7 @@
'
args:
creates: "{{ [ (MAGENTO_VOLUME | docker_volume_path), 'bin/magento' ] | path_join }}"
when: IS_STACK_HOST | bool
- name: "Run Magento setup:install (in container)"
command: >
@@ -48,5 +49,6 @@
(magento_install.stdout is defined and
('Magento installation complete' in magento_install.stdout
or 'successfully installed' in magento_install.stdout))
when: IS_STACK_HOST | bool
- include_tasks: utils/once/flag.yml
+3 -42
View File
@@ -36,45 +36,6 @@
webserver_vhost_flavour: "basic"
domain: "{{ MAILU_HOSTNAME }}"
- name: "Flush Docker Compose handlers"
meta: flush_handlers
- name: Wait for Mailu MariaDB
include_tasks: 02_wait_for_mariadb.yml
- name: Wait for Mailu Redis
include_tasks: 03_wait_for_redis.yml
- name: "Create Mailu accounts"
include_tasks: 04_manage_user.yml
vars:
mailu_action: >-
{{
(
'administrator' in (user_item.value.get('roles', []))
)
| ternary('admin','user')
}}
mailu_user_key: "{{ user_item.key }}"
mailu_user_name: "{{ user_item.value.username }}"
mailu_user_password: "{{ user_item.value.password }}"
mailu_token_ip: "{{ user_item.value.ip | default(networks.internet.ip4) }}"
mailu_token_name: "{{ SOFTWARE_NAME ~ ' Token for ' ~ user_item.value.username }}"
mailu_user_roles: "{{ user_item.value.roles | default([]) }}"
loop: "{{ lookup('users') | dict2items }}"
loop_control:
loop_var: user_item
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Include Cert deploy service for '{{ role_name }}'"
include_role:
name: sys-ctl-mtn-cert-deploy
when: lookup('tls', application_id, 'enabled') | bool
# Alarm mail setup reaches sys-svc-mail and therefore requires the Mailu
# API token that is created in 04_manage_user.yml.
- name: Generate DKIM public key
include_tasks: 05_generate-and-read-dkim.yml
- name: Set Mailu DNS records
include_tasks: 06_dns-records.yml
- name: "Mailu manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,41 @@
---
- name: "Flush Docker Compose handlers"
meta: flush_handlers
- name: Wait for Mailu MariaDB
include_tasks: 02_wait_for_mariadb.yml
- name: Wait for Mailu Redis
include_tasks: 03_wait_for_redis.yml
- name: "Create Mailu accounts"
include_tasks: 04_manage_user.yml
vars:
mailu_action: >-
{{
(
'administrator' in (user_item.value.get('roles', []))
)
| ternary('admin','user')
}}
mailu_user_key: "{{ user_item.key }}"
mailu_user_name: "{{ user_item.value.username }}"
mailu_user_password: "{{ user_item.value.password }}"
mailu_token_ip: "{{ user_item.value.ip | default(networks.internet.ip4) }}"
mailu_token_name: "{{ SOFTWARE_NAME ~ ' Token for ' ~ user_item.value.username }}"
mailu_user_roles: "{{ user_item.value.roles | default([]) }}"
loop: "{{ lookup('users') | dict2items }}"
loop_control:
loop_var: user_item
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Include Cert deploy service for '{{ role_name }}'"
include_role:
name: sys-ctl-mtn-cert-deploy
when: lookup('tls', application_id, 'enabled') | bool
- name: Generate DKIM public key
include_tasks: 05_generate-and-read-dkim.yml
- name: Set Mailu DNS records
include_tasks: 06_dns-records.yml
+3 -72
View File
@@ -1,5 +1,4 @@
---
# Required to be set on the top so global frontend service loading can rely on run_once guards.
- include_tasks: utils/once/flag.yml
- name: "Matomo | Resolve token from store/config"
@@ -9,76 +8,8 @@
include_role:
name: sys-stk-full
vars:
# Suppress flushing of handlers, because bootstrap file needs to be deployed first
compose_handlers_flush: false
- name: "Deploy '{{ MATOMO_BOOTSTRAP_ENV_FILE }}'"
template:
src: "templates/bootstrap.env.j2"
dest: "{{ MATOMO_BOOTSTRAP_ENV_FILE }}"
mode: "0640"
force: true
notify: compose up
- name: flush docker service
meta: flush_handlers
- name: "Matomo install | Wait for Matomo container to be running (any state)"
ansible.builtin.command: >
container inspect -f '{{ "{{" }}.State.Running{{ "}}" }}' {{ MATOMO_NAME }}
register: matomo_running
changed_when: false
retries: 30
delay: 2
until: >
matomo_running.rc == 0 and (matomo_running.stdout | trim) == 'true'
- name: "Matomo install | Check whether Matomo is already installed (config.ini.php exists)"
command:
argv:
- container
- exec
- "{{ MATOMO_NAME }}"
- sh
- -lc
- 'test -f "{{ MATOMO_CONFIG }}"'
register: matomo_config_exists
changed_when: false
failed_when: false
- name: "Bootstrap Matomo (Initialisation and setting of API token if not defined)"
include_tasks: 03a_bootstrap.yml
when: >
(matomo_config_exists.rc != 0) or
((lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim) == "")
# `04_patch_database.yml` rewrites the DB host/name/user/password lines in
# `config.ini.php` to whatever the current deploy renders. Without this, a
# matrix-deploy second round restarts the matomo container against the
# round-2 MariaDB user/password while `config.ini.php` still carries the
# round-1 credentials, and the healthcheck loops forever on
# "Database access denied". The patch MUST run before the wait-for-healthy
# probe so the container has the correct credentials by the time the
# probe gates the rest of the play.
- name: "Patch database credentials for {{ application_id }}"
include_tasks: 04_patch_database.yml
when: matomo_config_exists.rc == 0
- name: "Patch Trusted Hosts for {{ application_id }}"
include_tasks: 05_patch_trusted_hosts.yml
- name: flush docker service
meta: flush_handlers
- name: "Matomo install | Wait for Matomo container to be healthy"
ansible.builtin.command: >
container inspect -f '{{ "{{" }}.State.Health.Status{{ "}}" }}' {{ MATOMO_NAME }}
register: matomo_health
changed_when: false
retries: 60
delay: 2
until: >
matomo_health.rc == 0 and (matomo_health.stdout | trim) == 'healthy'
- name: "Config '{{ application_id }}' via API"
include_tasks: 06_config.yml
- name: "Matomo manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,71 @@
---
- name: "Deploy '{{ MATOMO_BOOTSTRAP_ENV_FILE }}'"
template:
src: "templates/bootstrap.env.j2"
dest: "{{ MATOMO_BOOTSTRAP_ENV_FILE }}"
mode: "0640"
force: true
notify: compose up
- name: flush docker service
meta: flush_handlers
- name: "Matomo install | Wait for Matomo container to be running (any state)"
ansible.builtin.command: >
container inspect -f '{{ "{{" }}.State.Running{{ "}}" }}' {{ MATOMO_NAME }}
register: matomo_running
changed_when: false
retries: 30
delay: 2
until: >
matomo_running.rc == 0 and (matomo_running.stdout | trim) == 'true'
- name: "Matomo install | Check whether Matomo is already installed (config.ini.php exists)"
command:
argv:
- container
- exec
- "{{ MATOMO_NAME }}"
- sh
- -lc
- 'test -f "{{ MATOMO_CONFIG }}"'
register: matomo_config_exists
changed_when: false
failed_when: false
- name: "Bootstrap Matomo (Initialisation and setting of API token if not defined)"
include_tasks: 03a_bootstrap.yml
when: >
(matomo_config_exists.rc != 0) or
((lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim) == "")
# `04_patch_database.yml` rewrites the DB host/name/user/password lines in
# `config.ini.php` to whatever the current deploy renders. Without this, a
# matrix-deploy second round restarts the matomo container against the
# round-2 MariaDB user/password while `config.ini.php` still carries the
# round-1 credentials, and the healthcheck loops forever on
# "Database access denied". The patch MUST run before the wait-for-healthy
# probe so the container has the correct credentials by the time the
# probe gates the rest of the play.
- name: "Patch database credentials for {{ application_id }}"
include_tasks: 04_patch_database.yml
when: matomo_config_exists.rc == 0
- name: "Patch Trusted Hosts for {{ application_id }}"
include_tasks: 05_patch_trusted_hosts.yml
- name: flush docker service
meta: flush_handlers
- name: "Matomo install | Wait for Matomo container to be healthy"
ansible.builtin.command: >
container inspect -f '{{ "{{" }}.State.Health.Status{{ "}}" }}' {{ MATOMO_NAME }}
register: matomo_health
changed_when: false
retries: 60
delay: 2
until: >
matomo_health.rc == 0 and (matomo_health.stdout | trim) == 'healthy'
- name: "Config '{{ application_id }}' via API"
include_tasks: 06_config.yml
+1
View File
@@ -10,3 +10,4 @@
src: "config.exs.j2"
dest: "{{ mobilizon_host_conf_exs_file }}"
notify: compose up
when: IS_STACK_HOST | bool
@@ -0,0 +1,85 @@
---
- name: "Populate Dockerfile build context (entrypoint, config.php, install scripts, php conf)"
include_tasks: 01_build_context.yml
- name: "Ensure nginx vhost dir exists in compose dir"
ansible.builtin.file:
path: "{{ docker_compose.directories.instance | default(lookup('container', application_id, 'directories.instance')) }}"
state: directory
mode: "0755"
- name: "Render nginx vhost into compose dir"
ansible.builtin.template:
src: "nginx-moodle.conf.j2"
dest: "{{ docker_compose.directories.instance | default(lookup('container', application_id, 'directories.instance')) }}/nginx.conf"
mode: "0640"
notify: compose up
- name: flush docker service
meta: flush_handlers
- name: Wait for Moodle container to become healthy
block:
- name: Wait until the Moodle container is healthy
ansible.builtin.shell: >
container inspect --format '{% raw %}{{.State.Health.Status}}{% endraw %}' {{ MOODLE_CONTAINER }}
register: health_check
until: health_check.stdout.strip() == "healthy"
retries: 120
delay: 5
rescue:
- name: Show Moodle container status
ansible.builtin.shell: >
container inspect {{ MOODLE_CONTAINER }}
register: moodle_inspect
changed_when: false
failed_when: false
- name: Show Moodle container logs
ansible.builtin.shell: >
container logs --tail=200 {{ MOODLE_CONTAINER }}
register: moodle_logs
changed_when: false
failed_when: false
- name: Fail with diagnostic output
ansible.builtin.fail:
msg: |
❌ Moodle container did not become healthy in time.
--- container inspect ---
{{ moodle_inspect.stdout | default('no output') }}
--- container logs (last 200 lines) ---
{{ moodle_logs.stdout | default('no output') }}
- name: "Run Moodle install (idempotent)"
command: >
container exec --user {{ MOODLE_RUNTIME_USER }} {{ MOODLE_CONTAINER }}
php {{ MOODLE_CLI_DIR }}/install_database.php
--agree-license
--adminuser={{ lookup('config', application_id, 'users.administrator.username') }}
--adminpass={{ lookup('config', application_id, 'credentials.user_password') | quote }}
--adminemail={{ lookup('config', application_id, 'users.administrator.email') }}
--fullname="{{ MOODLE_SITE_TITLE }}"
--shortname=Academy
register: moodle_install
changed_when: "'Moodle has been installed' in moodle_install.stdout"
failed_when:
- moodle_install.rc != 0
- "'Moodle is already installed' not in moodle_install.stderr"
- "'already installed' not in moodle_install.stdout"
- "'Database tables already present' not in moodle_install.stderr"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Include ownership settings tasks for moodle"
include_tasks: 02_ownership.yml
- name: "Configure OIDC login for Moodle (variant 0 only)"
include_tasks: 03_oidc.yml
when: MOODLE_OIDC_ENABLED | bool
- name: "Configure auth_ldap (sync-only in variant 0, primary login in variant 1)"
include_tasks: 04_ldap_sync.yml
when: MOODLE_LDAP_ENABLED | bool
+3 -84
View File
@@ -5,87 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Populate Dockerfile build context (entrypoint, config.php, install scripts, php conf)"
include_tasks: 01_build_context.yml
- name: "Ensure nginx vhost dir exists in compose dir"
ansible.builtin.file:
path: "{{ docker_compose.directories.instance | default(lookup('container', application_id, 'directories.instance')) }}"
state: directory
mode: "0755"
- name: "Render nginx vhost into compose dir"
ansible.builtin.template:
src: "nginx-moodle.conf.j2"
dest: "{{ docker_compose.directories.instance | default(lookup('container', application_id, 'directories.instance')) }}/nginx.conf"
mode: "0640"
notify: compose up
- name: flush docker service
meta: flush_handlers
- name: Wait for Moodle container to become healthy
block:
- name: Wait until the Moodle container is healthy
ansible.builtin.shell: >
container inspect --format '{% raw %}{{.State.Health.Status}}{% endraw %}' {{ MOODLE_CONTAINER }}
register: health_check
until: health_check.stdout.strip() == "healthy"
retries: 120
delay: 5
rescue:
- name: Show Moodle container status
ansible.builtin.shell: >
container inspect {{ MOODLE_CONTAINER }}
register: moodle_inspect
changed_when: false
failed_when: false
- name: Show Moodle container logs
ansible.builtin.shell: >
container logs --tail=200 {{ MOODLE_CONTAINER }}
register: moodle_logs
changed_when: false
failed_when: false
- name: Fail with diagnostic output
ansible.builtin.fail:
msg: |
❌ Moodle container did not become healthy in time.
--- container inspect ---
{{ moodle_inspect.stdout | default('no output') }}
--- container logs (last 200 lines) ---
{{ moodle_logs.stdout | default('no output') }}
- name: "Run Moodle install (idempotent)"
command: >
container exec --user {{ MOODLE_RUNTIME_USER }} {{ MOODLE_CONTAINER }}
php {{ MOODLE_CLI_DIR }}/install_database.php
--agree-license
--adminuser={{ lookup('config', application_id, 'users.administrator.username') }}
--adminpass={{ lookup('config', application_id, 'credentials.user_password') | quote }}
--adminemail={{ lookup('config', application_id, 'users.administrator.email') }}
--fullname="{{ MOODLE_SITE_TITLE }}"
--shortname=Academy
register: moodle_install
changed_when: "'Moodle has been installed' in moodle_install.stdout"
failed_when:
- moodle_install.rc != 0
- "'Moodle is already installed' not in moodle_install.stderr"
- "'already installed' not in moodle_install.stdout"
- "'Database tables already present' not in moodle_install.stderr"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Include ownership settings tasks for moodle"
include_tasks: 02_ownership.yml
- name: "Configure OIDC login for Moodle (variant 0 only)"
include_tasks: 03_oidc.yml
when: MOODLE_OIDC_ENABLED | bool
- name: "Configure auth_ldap (sync-only in variant 0, primary login in variant 1)"
include_tasks: 04_ldap_sync.yml
when: MOODLE_LDAP_ENABLED | bool
- name: "web-app-moodle manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,54 @@
---
- name: "Create Odoo addons directory"
file:
path: "{{ ODOO_ADDONS_HOST }}"
state: directory
mode: "0755"
- name: "Deploy Odoo configuration file"
template:
src: "odoo.conf.j2"
dest: "{{ ODOO_CONF_FILE_HOST }}"
mode: "0644"
notify:
- compose up
- name: "Copy custom addons"
copy:
src: "addons/"
dest: "{{ ODOO_ADDONS_HOST }}"
mode: "0644"
notify:
- compose up
- compose build
- name: "Docker Compose Up for '{{ application_id }}'"
meta: flush_handlers
- name: "Wait for Odoo to be ready"
uri_retry:
url: "http://127.0.0.1:{{ lookup('config', application_id, 'services.odoo.ports.local.http') }}/web/health"
status_code: 200
timeout: 10
register: odoo_health
until: odoo_health.status == 200
retries: 30
delay: 10
ignore_errors: true
- name: "Initialize Odoo database"
include_tasks: 01_init_database.yml
- name: "Install core modules"
include_tasks: 02_install_modules.yml
- name: "Create administrator user"
include_tasks: 05_admin.yml
- name: "Configure OIDC provider"
include_tasks: 03_oidc.yml
when: ODOO_OIDC_ENABLED | bool
- name: "Configure LDAP authentication"
include_tasks: 04_ldap.yml
when: ODOO_LDAP_ENABLED | bool
+3 -53
View File
@@ -5,56 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Create Odoo addons directory"
file:
path: "{{ ODOO_ADDONS_HOST }}"
state: directory
mode: "0755"
- name: "Deploy Odoo configuration file"
template:
src: "odoo.conf.j2"
dest: "{{ ODOO_CONF_FILE_HOST }}"
mode: "0644"
notify:
- compose up
- name: "Copy custom addons"
copy:
src: "addons/"
dest: "{{ ODOO_ADDONS_HOST }}"
mode: "0644"
notify:
- compose up
- compose build
- name: "Docker Compose Up for '{{ application_id }}'"
meta: flush_handlers
- name: "Wait for Odoo to be ready"
uri_retry:
url: "http://127.0.0.1:{{ lookup('config', application_id, 'services.odoo.ports.local.http') }}/web/health"
status_code: 200
timeout: 10
register: odoo_health
until: odoo_health.status == 200
retries: 30
delay: 10
ignore_errors: true
- name: "Initialize Odoo database"
include_tasks: 01_init_database.yml
- name: "Install core modules"
include_tasks: 02_install_modules.yml
- name: "Create administrator user"
include_tasks: 05_admin.yml
- name: "Configure OIDC provider"
include_tasks: 03_oidc.yml
when: ODOO_OIDC_ENABLED | bool
- name: "Configure LDAP authentication"
include_tasks: 04_ldap.yml
when: ODOO_LDAP_ENABLED | bool
- name: "web-app-odoo manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,33 @@
---
- name: "Create {{ OPENPROJECT_PLUGINS_FOLDER }}"
file:
path: "{{ OPENPROJECT_PLUGINS_FOLDER }}"
state: directory
mode: "0755"
- name: "Transferring Gemfile to {{ OPENPROJECT_PLUGINS_FOLDER }} as Gemfile.plugins"
copy:
src: Gemfile
dest: "{{ [OPENPROJECT_PLUGINS_FOLDER,'Gemfile.plugins'] | path_join }}"
notify:
- compose up
- compose build
- name: "create {{ OPENPROJECT_DUMMY_DIR }}"
file:
path: "{{ OPENPROJECT_DUMMY_DIR }}"
state: directory
mode: "0755"
- name: flush docker service
meta: flush_handlers
- name: Deploy Settings
include_tasks: 01_settings.yml
- name: Setup LDAP
include_tasks: 02_ldap.yml
when: OPENPROJECT_LDAP_ENABLED | bool
- name: Create OpenProject Administrator
include_tasks: 03_admin.yml
+3 -32
View File
@@ -10,35 +10,6 @@
compose_handlers_flush: false
application_id: "web-app-openproject"
- name: "Create {{ OPENPROJECT_PLUGINS_FOLDER }}"
file:
path: "{{ OPENPROJECT_PLUGINS_FOLDER }}"
state: directory
mode: "0755"
- name: "Transferring Gemfile to {{ OPENPROJECT_PLUGINS_FOLDER }} as Gemfile.plugins"
copy:
src: Gemfile
dest: "{{ [OPENPROJECT_PLUGINS_FOLDER,'Gemfile.plugins'] | path_join }}"
notify:
- compose up
- compose build
- name: "create {{ OPENPROJECT_DUMMY_DIR }}"
file:
path: "{{ OPENPROJECT_DUMMY_DIR }}"
state: directory
mode: "0755"
- name: flush docker service
meta: flush_handlers
- name: Deploy Settings
include_tasks: 01_settings.yml
- name: Setup LDAP
include_tasks: 02_ldap.yml
when: OPENPROJECT_LDAP_ENABLED | bool
- name: Create OpenProject Administrator
include_tasks: 03_admin.yml
- name: "OpenProject manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,154 @@
---
- name: "Render OpenTalk controller config '{{ application_id }}'"
ansible.builtin.template:
src: controller.toml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'controller.toml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Render LiveKit config '{{ application_id }}'"
ansible.builtin.template:
src: livekit.yaml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'livekit.yaml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Render OpenTalk recorder config '{{ application_id }}'"
ansible.builtin.template:
src: recorder.toml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'recorder.toml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Flush handlers for '{{ application_id }}' so compose up sees rendered configs"
meta: flush_handlers
# The shared `infinito.nexus` realm client is public, so it cannot
# introspect access tokens. Provision a dedicated confidential client
# (`opentalk-controller`) with a service account that can call the
# Keycloak admin API and the OIDC introspection endpoint.
- name: "Authenticate kcadm against the master realm (for OpenTalk client provisioning)"
ansible.builtin.shell: |-
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
set -e
{{ KEYCLOAK_KCADM }} config credentials \
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
--realm master \
--user "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
'
vars:
KEYCLOAK_EXEC_CONTAINER: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }}"
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Upsert dedicated confidential Keycloak client for the OpenTalk controller"
keycloak_kcadm_update:
object_kind: "client"
lookup_value: "{{ OPENTALK_CTRL_OIDC_CLIENT_ID }}"
desired:
clientId: "{{ OPENTALK_CTRL_OIDC_CLIENT_ID }}"
name: "OpenTalk Controller"
description: "Confidential client used by web-app-opentalk for token introspection and Keycloak admin API user search."
enabled: true
protocol: "openid-connect"
publicClient: false
bearerOnly: false
standardFlowEnabled: false
implicitFlowEnabled: false
directAccessGrantsEnabled: false
serviceAccountsEnabled: true
clientAuthenticatorType: "client-secret"
secret: "{{ OPENTALK_CTRL_OIDC_CLIENT_SECRET }}"
attributes:
"client.secret.creation.time": "0"
"oauth2.device.authorization.grant.enabled": "false"
"use.refresh.tokens": "true"
force_attrs:
publicClient: false
serviceAccountsEnabled: true
secret: "{{ OPENTALK_CTRL_OIDC_CLIENT_SECRET }}"
kcadm_exec: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }} /opt/keycloak/bin/kcadm.sh"
realm: "{{ OIDC.CLIENT.REALM }}"
assert_mode: "{{ MODE_ASSERT | bool }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Upsert dedicated confidential Keycloak client for the OpenTalk recorder"
keycloak_kcadm_update:
object_kind: "client"
lookup_value: "{{ OPENTALK_RECORDER_OIDC_CLIENT_ID }}"
desired:
clientId: "{{ OPENTALK_RECORDER_OIDC_CLIENT_ID }}"
name: "OpenTalk Recorder"
description: "Confidential service-account client used by the OpenTalk recorder to authenticate against the controller."
enabled: true
protocol: "openid-connect"
publicClient: false
bearerOnly: false
standardFlowEnabled: false
implicitFlowEnabled: false
directAccessGrantsEnabled: false
serviceAccountsEnabled: true
clientAuthenticatorType: "client-secret"
secret: "{{ OPENTALK_RECORDER_OIDC_CLIENT_SECRET }}"
attributes:
"client.secret.creation.time": "0"
"oauth2.device.authorization.grant.enabled": "false"
"use.refresh.tokens": "true"
force_attrs:
publicClient: false
serviceAccountsEnabled: true
secret: "{{ OPENTALK_RECORDER_OIDC_CLIENT_SECRET }}"
kcadm_exec: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }} /opt/keycloak/bin/kcadm.sh"
realm: "{{ OIDC.CLIENT.REALM }}"
assert_mode: "{{ MODE_ASSERT | bool }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Grant the OpenTalk controller's service account the realm-management `view-users` role"
ansible.builtin.shell: |-
{{ KCEXEC }} sh -lc '
set -e
KCADM={{ KEYCLOAK_KCADM }}
REALM={{ OIDC.CLIENT.REALM | quote }}
CLIENT_ID={{ OPENTALK_CTRL_OIDC_CLIENT_ID | quote }}
SA_USER_ID=$($KCADM get clients/$($KCADM get clients -r $REALM -q clientId=$CLIENT_ID --fields id --format csv --noquotes | tail -n1)/service-account-user -r $REALM --fields id --format csv --noquotes | tail -n1)
RM_CLIENT_ID=$($KCADM get clients -r $REALM -q clientId=realm-management --fields id --format csv --noquotes | tail -n1)
$KCADM add-roles -r $REALM --uusername service-account-$CLIENT_ID --cclientid realm-management --rolename view-users --rolename query-users || true
'
vars:
KCEXEC: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }}"
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Restart the OpenTalk controller so it picks up the refreshed introspection client"
ansible.builtin.command: "container restart {{ OPENTALK_CTRL_CONTAINER }}"
changed_when: true
- name: "Wait for MinIO to accept the configured root credentials"
ansible.builtin.shell: >
container exec {{ OPENTALK_MINIO_CONTAINER }}
mc alias set local http://127.0.0.1:9000
{{ OPENTALK_MINIO_USER | quote }}
{{ OPENTALK_MINIO_PASSWORD | quote }}
register: minio_ready
retries: 60
delay: 3
until: minio_ready.rc == 0
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Ensure OpenTalk MinIO bucket exists"
ansible.builtin.shell: >
container exec {{ OPENTALK_MINIO_CONTAINER }}
mc mb local/{{ OPENTALK_MINIO_BUCKET }}
register: minio_bucket
changed_when: "'Bucket created successfully' in minio_bucket.stdout"
failed_when: minio_bucket.rc != 0 and 'already own it' not in minio_bucket.stderr
+3 -155
View File
@@ -5,158 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Render OpenTalk controller config '{{ application_id }}'"
ansible.builtin.template:
src: controller.toml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'controller.toml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Render LiveKit config '{{ application_id }}'"
ansible.builtin.template:
src: livekit.yaml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'livekit.yaml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Render OpenTalk recorder config '{{ application_id }}'"
ansible.builtin.template:
src: recorder.toml.j2
dest: "{{ [lookup('container', application_id, 'directories.volumes'), 'recorder.toml'] | path_join }}"
mode: "0644"
notify:
- compose up
- name: "Flush handlers for '{{ application_id }}' so compose up sees rendered configs"
meta: flush_handlers
# The shared `infinito.nexus` realm client is public, so it cannot
# introspect access tokens. Provision a dedicated confidential client
# (`opentalk-controller`) with a service account that can call the
# Keycloak admin API and the OIDC introspection endpoint.
- name: "Authenticate kcadm against the master realm (for OpenTalk client provisioning)"
ansible.builtin.shell: |-
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
set -e
{{ KEYCLOAK_KCADM }} config credentials \
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
--realm master \
--user "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
'
vars:
KEYCLOAK_EXEC_CONTAINER: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }}"
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Upsert dedicated confidential Keycloak client for the OpenTalk controller"
keycloak_kcadm_update:
object_kind: "client"
lookup_value: "{{ OPENTALK_CTRL_OIDC_CLIENT_ID }}"
desired:
clientId: "{{ OPENTALK_CTRL_OIDC_CLIENT_ID }}"
name: "OpenTalk Controller"
description: "Confidential client used by web-app-opentalk for token introspection and Keycloak admin API user search."
enabled: true
protocol: "openid-connect"
publicClient: false
bearerOnly: false
standardFlowEnabled: false
implicitFlowEnabled: false
directAccessGrantsEnabled: false
serviceAccountsEnabled: true
clientAuthenticatorType: "client-secret"
secret: "{{ OPENTALK_CTRL_OIDC_CLIENT_SECRET }}"
attributes:
"client.secret.creation.time": "0"
"oauth2.device.authorization.grant.enabled": "false"
"use.refresh.tokens": "true"
force_attrs:
publicClient: false
serviceAccountsEnabled: true
secret: "{{ OPENTALK_CTRL_OIDC_CLIENT_SECRET }}"
kcadm_exec: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }} /opt/keycloak/bin/kcadm.sh"
realm: "{{ OIDC.CLIENT.REALM }}"
assert_mode: "{{ MODE_ASSERT | bool }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Upsert dedicated confidential Keycloak client for the OpenTalk recorder"
keycloak_kcadm_update:
object_kind: "client"
lookup_value: "{{ OPENTALK_RECORDER_OIDC_CLIENT_ID }}"
desired:
clientId: "{{ OPENTALK_RECORDER_OIDC_CLIENT_ID }}"
name: "OpenTalk Recorder"
description: "Confidential service-account client used by the OpenTalk recorder to authenticate against the controller."
enabled: true
protocol: "openid-connect"
publicClient: false
bearerOnly: false
standardFlowEnabled: false
implicitFlowEnabled: false
directAccessGrantsEnabled: false
serviceAccountsEnabled: true
clientAuthenticatorType: "client-secret"
secret: "{{ OPENTALK_RECORDER_OIDC_CLIENT_SECRET }}"
attributes:
"client.secret.creation.time": "0"
"oauth2.device.authorization.grant.enabled": "false"
"use.refresh.tokens": "true"
force_attrs:
publicClient: false
serviceAccountsEnabled: true
secret: "{{ OPENTALK_RECORDER_OIDC_CLIENT_SECRET }}"
kcadm_exec: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }} /opt/keycloak/bin/kcadm.sh"
realm: "{{ OIDC.CLIENT.REALM }}"
assert_mode: "{{ MODE_ASSERT | bool }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Grant the OpenTalk controller's service account the realm-management `view-users` role"
ansible.builtin.shell: |-
{{ KCEXEC }} sh -lc '
set -e
KCADM={{ KEYCLOAK_KCADM }}
REALM={{ OIDC.CLIENT.REALM | quote }}
CLIENT_ID={{ OPENTALK_CTRL_OIDC_CLIENT_ID | quote }}
# Resolve the service-account user for the controller client.
SA_USER_ID=$($KCADM get clients/$($KCADM get clients -r $REALM -q clientId=$CLIENT_ID --fields id --format csv --noquotes | tail -n1)/service-account-user -r $REALM --fields id --format csv --noquotes | tail -n1)
# Resolve the realm-management client id and the view-users role payload.
RM_CLIENT_ID=$($KCADM get clients -r $REALM -q clientId=realm-management --fields id --format csv --noquotes | tail -n1)
$KCADM add-roles -r $REALM --uusername service-account-$CLIENT_ID --cclientid realm-management --rolename view-users --rolename query-users || true
'
vars:
KCEXEC: "container exec -i {{ lookup('config', 'web-app-keycloak', 'services.keycloak.name') }}"
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Restart the OpenTalk controller so it picks up the refreshed introspection client"
ansible.builtin.command: "container restart {{ OPENTALK_CTRL_CONTAINER }}"
changed_when: true
- name: "Wait for MinIO to accept the configured root credentials"
ansible.builtin.shell: >
container exec {{ OPENTALK_MINIO_CONTAINER }}
mc alias set local http://127.0.0.1:9000
{{ OPENTALK_MINIO_USER | quote }}
{{ OPENTALK_MINIO_PASSWORD | quote }}
register: minio_ready
retries: 60
delay: 3
until: minio_ready.rc == 0
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Ensure OpenTalk MinIO bucket exists"
ansible.builtin.shell: >
container exec {{ OPENTALK_MINIO_CONTAINER }}
mc mb local/{{ OPENTALK_MINIO_BUCKET }}
register: minio_bucket
changed_when: "'Bucket created successfully' in minio_bucket.stdout"
failed_when: minio_bucket.rc != 0 and 'already own it' not in minio_bucket.stderr
- name: "OpenTalk manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
+3 -31
View File
@@ -7,34 +7,6 @@
vars:
compose_handlers_flush: false
# Keep the custom entrypoint on disk before the first compose up. If this is
# moved below the flush, Pixelfed can boot once with the upstream entrypoint and
# get stuck in the half-initialized state this role is meant to recover from.
- name: "Deploy '{{ PIXELFED_ENTRYPOINT_HOST_ABS }}'"
ansible.builtin.copy:
src: "{{ PIXELFED_ENTRYPOINT_FILE }}"
dest: "{{ PIXELFED_ENTRYPOINT_HOST_ABS }}"
mode: "0755"
notify: compose up
- name: "Ensure Pixelfed .env ownership and permissions for www-data"
ansible.builtin.file:
path: "{{ PIXELFED_ENV_FILE }}"
owner: "{{ PIXELFED_APP_USER }}"
group: "{{ PIXELFED_APP_USER }}"
mode: "0660"
# Flush here on purpose: the application container must start with the
# repo-managed entrypoint before 02_bootstrap.yml runs any artisan bootstrap.
- name: "Start Pixelfed containers with the repo-managed entrypoint"
ansible.builtin.meta: flush_handlers
- name: "Bootstrap the Pixelfed application"
ansible.builtin.import_tasks: 02_bootstrap.yml
- name: "Bootstrap the Pixelfed administrator"
ansible.builtin.import_tasks: 03_administrator.yml
- name: "Bootstrap the Pixelfed administrator OIDC mapping"
ansible.builtin.import_tasks: 04_oidc_mapping.yml
when: PIXELFED_OIDC_ENABLED | bool
- name: "Pixelfed manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,32 @@
---
# Keep the custom entrypoint on disk before the first compose up. If this is
# moved below the flush, Pixelfed can boot once with the upstream entrypoint and
# get stuck in the half-initialized state this role is meant to recover from.
- name: "Deploy '{{ PIXELFED_ENTRYPOINT_HOST_ABS }}'"
ansible.builtin.copy:
src: "{{ PIXELFED_ENTRYPOINT_FILE }}"
dest: "{{ PIXELFED_ENTRYPOINT_HOST_ABS }}"
mode: "0755"
notify: compose up
- name: "Ensure Pixelfed .env ownership and permissions for www-data"
ansible.builtin.file:
path: "{{ PIXELFED_ENV_FILE }}"
owner: "{{ PIXELFED_APP_USER }}"
group: "{{ PIXELFED_APP_USER }}"
mode: "0660"
# Flush here on purpose: the application container must start with the
# repo-managed entrypoint before 02_bootstrap.yml runs any artisan bootstrap.
- name: "Start Pixelfed containers with the repo-managed entrypoint"
ansible.builtin.meta: flush_handlers
- name: "Bootstrap the Pixelfed application"
ansible.builtin.import_tasks: 02_bootstrap.yml
- name: "Bootstrap the Pixelfed administrator"
ansible.builtin.import_tasks: 03_administrator.yml
- name: "Bootstrap the Pixelfed administrator OIDC mapping"
ansible.builtin.import_tasks: 04_oidc_mapping.yml
when: PIXELFED_OIDC_ENABLED | bool
@@ -0,0 +1,30 @@
---
- name: "Deploy {{ SHOPWARE_INIT_HOST }}"
copy:
src: init.sh
dest: "{{ SHOPWARE_INIT_HOST }}"
mode: "0755"
notify:
- compose up
- compose build
- name: "Render framework.yaml (trusted proxies/headers/hosts)"
copy:
src: "framework.yaml"
dest: "{{ SHOPWARE_FRAMEWORK_HOST }}"
mode: "0644"
notify:
- compose up
- name: "Flush compose handlers"
meta: flush_handlers
- name: Wait for Shopware HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.shopware.ports.local.http') }}"
delay: 5
timeout: 300
- name: "Ensure admin user exists with correct password"
include_tasks: 01_admin.yml
+3 -36
View File
@@ -5,39 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Deploy {{ SHOPWARE_INIT_HOST }}"
copy:
src: init.sh
dest: "{{ SHOPWARE_INIT_HOST }}"
mode: "0755"
notify:
- compose up
- compose build
- name: "Render framework.yaml (trusted proxies/headers/hosts)"
copy:
src: "framework.yaml"
dest: "{{ SHOPWARE_FRAMEWORK_HOST }}"
mode: "0644"
notify:
- compose up
- name: "Flush compose handlers"
meta: flush_handlers
- name: Wait for Shopware HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.shopware.ports.local.http') }}"
delay: 5
timeout: 300
- name: "Ensure admin user exists with correct password"
include_tasks: 01_admin.yml
#- name: Execute setup routines (OIDC/LDAP)
# include_tasks: 02_setup.yml
#
#- name: Execute cleanup routines
# include_tasks: 03_cleanup.yml
# when: MODE_CLEANUP
- name: "Shopware manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,27 @@
---
- name: "Deploy PHP error configuration (env aware)"
template:
src: "php.ini.j2"
dest: "{{ SUITECRM_PHP_CONFIG_HOST }}"
notify:
- compose up
- name: "Render SuiteCRM LDAP mapping (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "ldap.yaml.j2"
dest: "{{ SUITECRM_LDAP_CONFIG_HOST }}"
when: "{{ SUITECRM_LDAP_ENABLED | bool }}"
- name: "Deploy '{{ SUITECRM_ENTRYPOINT_SCRIPT_HOST_ABS }}'"
copy:
src: "{{ SUITECRM_ENTRYPOINT_SCRIPT_FILE }}"
dest: "{{ SUITECRM_ENTRYPOINT_SCRIPT_HOST_ABS }}"
notify:
- compose up
- compose build
- name: "Docker Compose Up for '{{ application_id }}'"
meta: flush_handlers
+3 -26
View File
@@ -5,29 +5,6 @@
vars:
compose_handlers_flush: false
- name: "Deploy PHP error configuration (env aware)"
template:
src: "php.ini.j2"
dest: "{{ SUITECRM_PHP_CONFIG_HOST }}"
notify:
- compose up
- name: "Render SuiteCRM LDAP mapping (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "ldap.yaml.j2"
dest: "{{ SUITECRM_LDAP_CONFIG_HOST }}"
when: "{{ SUITECRM_LDAP_ENABLED | bool }}"
- name: "Deploy '{{ SUITECRM_ENTRYPOINT_SCRIPT_HOST_ABS }}'"
copy:
src: "{{ SUITECRM_ENTRYPOINT_SCRIPT_FILE }}"
dest: "{{ SUITECRM_ENTRYPOINT_SCRIPT_HOST_ABS }}"
notify:
- compose up
- compose build
- name: "Docker Compose Up for '{{ application_id }}'"
meta: flush_handlers
- name: "SuiteCRM manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,54 @@
---
- name: "copy taiga gateway config with OIDC route"
template:
src: "taiga-gateway/taiga.conf.j2"
dest: "{{ [compose_repository_path, 'taiga-gateway', 'taiga.conf'] | path_join }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "render Dockerfile.taiga-back for custom auth-enabled backend image"
template:
src: "Dockerfile.backend.j2"
dest: "{{ [compose_repository_path, 'Dockerfile.taiga-back'] | path_join }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "render Dockerfile.taiga-front for custom OIDC frontend image"
template:
src: "Dockerfile.frontend.j2"
dest: "{{ [compose_repository_path, 'Dockerfile.taiga-front'] | path_join }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "copy taiga config.append.py (OIDC and/or LDAP settings appended to config.py at runtime)"
template:
src: "settings/config.append.py.j2"
dest: "{{ TAIGA_CONFIG_APPEND_PATH }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "copy taiga urls.py (OIDC URL routing)"
template:
src: "settings/urls.py.j2"
dest: "{{ TAIGA_URLS_PATH }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "copy taiga frontend conf.json (loginFormType / contribPlugins)"
template:
src: "settings/conf.json.j2"
dest: "{{ TAIGA_FRONTEND_CONF_PATH }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "create {{ TAIGA_COMPOSE_INIT_PATH }}"
template:
src: "compose-inits.yml.j2"
dest: "{{ TAIGA_COMPOSE_INIT_PATH }}"
notify: compose up
- name: "Include Taiga administrator routines"
include_tasks: 01_administrator.yml
- name: "Flush Taiga handlers"
meta: flush_handlers
+3 -53
View File
@@ -8,56 +8,6 @@
compose_repository_ref: "{{ TAIGA_REF }}"
compose_repository_pull: true
- name: "copy taiga gateway config with OIDC route"
template:
src: "taiga-gateway/taiga.conf.j2"
dest: "{{ [compose_repository_path, 'taiga-gateway', 'taiga.conf'] | path_join }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "render Dockerfile.taiga-back for custom auth-enabled backend image"
template:
src: "Dockerfile.backend.j2"
dest: "{{ [compose_repository_path, 'Dockerfile.taiga-back'] | path_join }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "render Dockerfile.taiga-front for custom OIDC frontend image"
template:
src: "Dockerfile.frontend.j2"
dest: "{{ [compose_repository_path, 'Dockerfile.taiga-front'] | path_join }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "copy taiga config.append.py (OIDC and/or LDAP settings appended to config.py at runtime)"
template:
src: "settings/config.append.py.j2"
dest: "{{ TAIGA_CONFIG_APPEND_PATH }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "copy taiga urls.py (OIDC URL routing)"
template:
src: "settings/urls.py.j2"
dest: "{{ TAIGA_URLS_PATH }}"
when: TAIGA_OIDC_ENABLED | bool
notify: compose up
- name: "copy taiga frontend conf.json (loginFormType / contribPlugins)"
template:
src: "settings/conf.json.j2"
dest: "{{ TAIGA_FRONTEND_CONF_PATH }}"
when: TAIGA_PLUGINS_ENABLED | bool
notify: compose up
- name: "create {{ TAIGA_COMPOSE_INIT_PATH }}"
template:
src: "compose-inits.yml.j2"
dest: "{{ TAIGA_COMPOSE_INIT_PATH }}"
notify: compose up
- name: "Include Taiga administrator routines"
include_tasks: 01_administrator.yml
- name: "Flush Taiga handlers"
meta: flush_handlers
- name: "Taiga manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
+4 -68
View File
@@ -11,77 +11,13 @@
- name: Deploy Bootstrap Config
include_tasks: _flush_config.yml
when: IS_STACK_HOST | bool
vars:
xwiki_oidc_enabled_switch: false
xwiki_ldap_enabled_switch: false
- name: "ASSERT | superadmin can authenticate (needed for installer)"
uri_retry:
url: "{{ [XWIKI_REST_XWIKI, 'spaces'] | url_join }}"
method: GET
url_username: "{{ XWIKI_SUPERADMIN_USERNAME }}"
url_password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200, 401, 403, 500, 503]
return_content: true
register: _super_check_ext
changed_when: false
failed_when: false
- block:
- name: "DEBUG | superadmin check + pw hash compare (condensed)"
script: >-
debug_superadmin.sh
"{{ _super_check_ext.status }}"
"{{ _super_check_ext.redirected | default('n/a') }}"
"{{ _super_check_ext.url | default('n/a') }}"
"{{ _super_check_ext.www_authenticate | default(_super_check_ext.headers['www-authenticate'] | default('n/a')) }}"
"{{ _super_check_ext.content_type | default(_super_check_ext.headers['content-type'] | default('n/a')) }}"
"{{ (_super_check_ext.content | default(''))[:600] | replace('\n','\\n') }}"
"{{ XWIKI_CONTAINER }}"
"{{ XWIKI_SUPERADMIN_PASSWORD }}"
register: _dbg_superadmin
changed_when: false
failed_when: false
- name: "FAIL | superadmin authentication failed (extensions phase)"
fail:
msg: |
superadmin authentication failed (extensions phase)
superadmin check status={{ _super_check_ext.status }}
www_authenticate={{
_super_check_ext.www_authenticate
| default(_super_check_ext.headers['www-authenticate'] | default('n/a'))
}}
debug:
{{ _dbg_superadmin.stdout | default('') }}
hint: check xwiki.cfg in image / password / Dockerfile build
when: _super_check_ext.status != 200
- name: Load setup procedures for admin
include_tasks: 03_administrator.yml
when: not (XWIKI_SSO_ENABLED | bool)
- name: Load setup procedures for extensions
include_tasks: 04_extensions.yml
- name: "XWIKI | Ensure Main.WebHome exists"
include_tasks: 05_main_webhome.yml
- block:
- name: "Create Final Docker Compose File"
template:
src: "compose.yml.j2"
dest: "{{ lookup('container', application_id, 'files.compose') }}"
notify: compose up
- name: Deploy Final Config
include_tasks: _flush_config.yml
vars:
xwiki_oidc_enabled_switch: "{{ XWIKI_OIDC_ENABLED | bool }}"
xwiki_ldap_enabled_switch: "{{ XWIKI_LDAP_ENABLED | bool }}"
- name: "XWiki manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
- include_tasks: utils/once/flag.yml
@@ -0,0 +1,69 @@
---
- name: "ASSERT | superadmin can authenticate (needed for installer)"
uri_retry:
url: "{{ [XWIKI_REST_XWIKI, 'spaces'] | url_join }}"
method: GET
url_username: "{{ XWIKI_SUPERADMIN_USERNAME }}"
url_password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200, 401, 403, 500, 503]
return_content: true
register: _super_check_ext
changed_when: false
failed_when: false
- block:
- name: "DEBUG | superadmin check + pw hash compare (condensed)"
script: >-
debug_superadmin.sh
"{{ _super_check_ext.status }}"
"{{ _super_check_ext.redirected | default('n/a') }}"
"{{ _super_check_ext.url | default('n/a') }}"
"{{ _super_check_ext.www_authenticate | default(_super_check_ext.headers['www-authenticate'] | default('n/a')) }}"
"{{ _super_check_ext.content_type | default(_super_check_ext.headers['content-type'] | default('n/a')) }}"
"{{ (_super_check_ext.content | default(''))[:600] | replace('\n','\\n') }}"
"{{ XWIKI_CONTAINER }}"
"{{ XWIKI_SUPERADMIN_PASSWORD }}"
register: _dbg_superadmin
changed_when: false
failed_when: false
- name: "FAIL | superadmin authentication failed (extensions phase)"
fail:
msg: |
superadmin authentication failed (extensions phase)
superadmin check status={{ _super_check_ext.status }}
www_authenticate={{
_super_check_ext.www_authenticate
| default(_super_check_ext.headers['www-authenticate'] | default('n/a'))
}}
debug:
{{ _dbg_superadmin.stdout | default('') }}
hint: check xwiki.cfg in image / password / Dockerfile build
when: _super_check_ext.status != 200
- name: Load setup procedures for admin
include_tasks: 03_administrator.yml
when: not (XWIKI_SSO_ENABLED | bool)
- name: Load setup procedures for extensions
include_tasks: 04_extensions.yml
- name: "XWIKI | Ensure Main.WebHome exists"
include_tasks: 05_main_webhome.yml
- block:
- name: "Create Final Docker Compose File"
template:
src: "compose.yml.j2"
dest: "{{ lookup('container', application_id, 'files.compose') }}"
notify: compose up
- name: Deploy Final Config
include_tasks: _flush_config.yml
vars:
xwiki_oidc_enabled_switch: "{{ XWIKI_OIDC_ENABLED | bool }}"
xwiki_ldap_enabled_switch: "{{ XWIKI_LDAP_ENABLED | bool }}"
+3 -137
View File
@@ -7,140 +7,6 @@
vars:
compose_handlers_flush: false
- name: "Stage Zammad host-side init files (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "auto_wizard.json.j2"
dest: "{{ ZAMMAD_AUTO_WIZARD_HOST_PATH }}"
mode: "0644"
- src: "zz_omniauth_allow_get.rb.j2"
dest: "{{ ZAMMAD_OMNIAUTH_INIT_HOST_PATH }}"
mode: "0644"
- src: "telegram_helper_stub.rb.j2"
dest: "{{ ZAMMAD_TELEGRAM_HELPER_HOST_PATH }}"
mode: "0644"
- src: "zz_oidc_group_role_mapping.rb.j2"
dest: "{{ ZAMMAD_OIDC_GROUP_ROLE_MAP_HOST_PATH }}"
mode: "0644"
when: "{{ ZAMMAD_OIDC_ENABLED }}"
- name: "grant CREATEDB to the Zammad PostgreSQL role (Zammad's init runs rake db:create even when the DB exists)"
database_query:
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
config: >-
{{ lookup('database', application_id) | combine({
'username': 'postgres',
'name': 'postgres',
'password': lookup('config', 'svc-db-postgres', 'credentials.POSTGRES_PASSWORD'),
}) }}
# nocheck: database-query-raw
query: "{{ lookup('template', role_path ~ '/templates/sql/grant_createdb.sql.j2') }}"
when: lookup('config', application_id, 'services.postgres.shared') | bool
- name: "flush handlers so the Zammad init container starts with auto_wizard.json bind-mounted"
meta: flush_handlers
- name: "wait for the Zammad Rails server to answer"
ansible.builtin.shell: |
set -eo pipefail
container exec {{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r 'Setting.any? || raise'
register: zammad_rails_ready
changed_when: false
retries: 60
delay: 5
until: zammad_rails_ready.rc == 0
- name: "process auto_wizard.json if Zammad's setup wizard hasn't completed yet (idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i {{ ZAMMAD_RAILS_CONTAINER }} bundle exec rails r '
if !Setting.get("system_init_done")
raise "AutoWizard data missing (auto_wizard.json not mounted in rails container)" unless AutoWizard.enabled?
AutoWizard.setup
end
'
register: zammad_auto_wizard_setup
changed_when: false
- name: "ensure API-bot user in Zammad (Basic-auth REST API; runs in EVERY variant including no-auth)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e API_BOT_LOGIN -e API_BOT_EMAIL -e API_BOT_FIRSTNAME -e API_BOT_LASTNAME -e API_BOT_PASSWORD \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_api_bot.rb
environment:
API_BOT_LOGIN: "{{ ZAMMAD_API_BOT_USER.username }}"
API_BOT_EMAIL: "{{ ZAMMAD_API_BOT_USER.email }}"
API_BOT_FIRSTNAME: "{{ ZAMMAD_API_BOT_USER.firstname }}"
API_BOT_LASTNAME: "{{ ZAMMAD_API_BOT_USER.lastname }}"
API_BOT_PASSWORD: "{{ ZAMMAD_API_BOT_PASSWORD }}"
register: zammad_api_bot_apply
changed_when: false
- name: "apply OIDC settings in Zammad (post-bootstrap; idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e ZAMMAD_FQDN -e ZAMMAD_HTTP_TYPE \
-e OIDC_BUTTON_TEXT -e OIDC_CLIENT_ID -e OIDC_CLIENT_SECRET -e OIDC_ISSUER_URL \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_oidc_settings.rb
environment:
ZAMMAD_FQDN: "{{ ZAMMAD_HOSTNAME }}"
ZAMMAD_HTTP_TYPE: "{{ ZAMMAD_WEB_PROTOCOL }}"
OIDC_BUTTON_TEXT: "{{ OIDC.BUTTON_TEXT }}"
OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
OIDC_ISSUER_URL: "{{ OIDC.CLIENT.ISSUER_URL }}"
register: zammad_oidc_apply
changed_when: false
when: ZAMMAD_OIDC_ENABLED
- name: "configure Zammad's LdapSource against svc-db-openldap (post-bootstrap; idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e LDAP_NAME -e LDAP_HOST -e LDAP_PORT -e LDAP_BASE_DN \
-e LDAP_BIND_DN -e LDAP_BIND_PW -e LDAP_UID_ATTR \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_ldap_source.rb
environment:
LDAP_NAME: "{{ ORGANIZATION }} svc-db-openldap"
LDAP_HOST: "{{ LDAP.SERVER.DOMAIN }}"
LDAP_PORT: "{{ LDAP.SERVER.PORT }}"
LDAP_BASE_DN: "{{ LDAP.DN.OU.USERS }}"
LDAP_BIND_DN: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
LDAP_BIND_PW: "{{ LDAP.BIND_CREDENTIAL }}"
LDAP_UID_ATTR: "{{ LDAP.USER.ATTRIBUTES.ID }}"
register: zammad_ldap_apply
changed_when: false
when: ZAMMAD_LDAP_ENABLED
- name: "configure Zammad's Email channel (inbound IMAP + outbound SMTP) against Mailu"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e MAIL_GROUP_NAME -e MAIL_FROM_NAME -e MAIL_FROM_ADDRESS \
-e IMAP_HOST -e IMAP_PORT -e IMAP_USER -e IMAP_PASS \
-e SMTP_HOST -e SMTP_PORT -e SMTP_USER -e SMTP_PASS \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_email_channel.rb
environment:
MAIL_GROUP_NAME: "Users"
MAIL_FROM_NAME: "{{ ORGANIZATION }} Helpdesk"
MAIL_FROM_ADDRESS: "{{ ZAMMAD_HELPDESK_EMAIL }}"
IMAP_HOST: "{{ ZAMMAD_MAIL.host }}"
IMAP_PORT: "143"
IMAP_USER: "{{ ZAMMAD_HELPDESK_EMAIL }}"
IMAP_PASS: "{{ ZAMMAD_HELPDESK_PASS }}"
SMTP_HOST: "{{ ZAMMAD_MAIL.host }}"
SMTP_PORT: "{{ ZAMMAD_MAIL.port }}"
SMTP_USER: "{{ ZAMMAD_HELPDESK_EMAIL }}"
SMTP_PASS: "{{ ZAMMAD_HELPDESK_PASS }}"
register: zammad_email_apply
changed_when: false
when: ZAMMAD_EMAIL_ENABLED and (ZAMMAD_HELPDESK_EMAIL | length > 0)
- name: "Zammad manager-side operations (file deploys + container exec)"
include_tasks: 02_manager_ops.yml
when: IS_STACK_HOST | bool
@@ -0,0 +1,138 @@
---
- name: "Stage Zammad host-side init files (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ [playbook_dir, 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml'] | path_join }}"
vars:
role_templates:
- src: "auto_wizard.json.j2"
dest: "{{ ZAMMAD_AUTO_WIZARD_HOST_PATH }}"
mode: "0644"
- src: "zz_omniauth_allow_get.rb.j2"
dest: "{{ ZAMMAD_OMNIAUTH_INIT_HOST_PATH }}"
mode: "0644"
- src: "telegram_helper_stub.rb.j2"
dest: "{{ ZAMMAD_TELEGRAM_HELPER_HOST_PATH }}"
mode: "0644"
- src: "zz_oidc_group_role_mapping.rb.j2"
dest: "{{ ZAMMAD_OIDC_GROUP_ROLE_MAP_HOST_PATH }}"
mode: "0644"
when: "{{ ZAMMAD_OIDC_ENABLED }}"
- name: "grant CREATEDB to the Zammad PostgreSQL role (Zammad's init runs rake db:create even when the DB exists)"
database_query:
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
config: >-
{{ lookup('database', application_id) | combine({
'username': 'postgres',
'name': 'postgres',
'password': lookup('config', 'svc-db-postgres', 'credentials.POSTGRES_PASSWORD'),
}) }}
# nocheck: database-query-raw
query: "{{ lookup('template', role_path ~ '/templates/sql/grant_createdb.sql.j2') }}"
when: lookup('config', application_id, 'services.postgres.shared') | bool
- name: "flush handlers so the Zammad init container starts with auto_wizard.json bind-mounted"
meta: flush_handlers
- name: "wait for the Zammad Rails server to answer"
ansible.builtin.shell: |
set -eo pipefail
container exec {{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r 'Setting.any? || raise'
register: zammad_rails_ready
changed_when: false
retries: 60
delay: 5
until: zammad_rails_ready.rc == 0
- name: "process auto_wizard.json if Zammad's setup wizard hasn't completed yet (idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i {{ ZAMMAD_RAILS_CONTAINER }} bundle exec rails r '
if !Setting.get("system_init_done")
raise "AutoWizard data missing (auto_wizard.json not mounted in rails container)" unless AutoWizard.enabled?
AutoWizard.setup
end
'
register: zammad_auto_wizard_setup
changed_when: false
- name: "ensure API-bot user in Zammad (Basic-auth REST API; runs in EVERY variant including no-auth)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e API_BOT_LOGIN -e API_BOT_EMAIL -e API_BOT_FIRSTNAME -e API_BOT_LASTNAME -e API_BOT_PASSWORD \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_api_bot.rb
environment:
API_BOT_LOGIN: "{{ ZAMMAD_API_BOT_USER.username }}"
API_BOT_EMAIL: "{{ ZAMMAD_API_BOT_USER.email }}"
API_BOT_FIRSTNAME: "{{ ZAMMAD_API_BOT_USER.firstname }}"
API_BOT_LASTNAME: "{{ ZAMMAD_API_BOT_USER.lastname }}"
API_BOT_PASSWORD: "{{ ZAMMAD_API_BOT_PASSWORD }}"
register: zammad_api_bot_apply
changed_when: false
- name: "apply OIDC settings in Zammad (post-bootstrap; idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e ZAMMAD_FQDN -e ZAMMAD_HTTP_TYPE \
-e OIDC_BUTTON_TEXT -e OIDC_CLIENT_ID -e OIDC_CLIENT_SECRET -e OIDC_ISSUER_URL \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_oidc_settings.rb
environment:
ZAMMAD_FQDN: "{{ ZAMMAD_HOSTNAME }}"
ZAMMAD_HTTP_TYPE: "{{ ZAMMAD_WEB_PROTOCOL }}"
OIDC_BUTTON_TEXT: "{{ OIDC.BUTTON_TEXT }}"
OIDC_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
OIDC_ISSUER_URL: "{{ OIDC.CLIENT.ISSUER_URL }}"
register: zammad_oidc_apply
changed_when: false
when: ZAMMAD_OIDC_ENABLED
- name: "configure Zammad's LdapSource against svc-db-openldap (post-bootstrap; idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e LDAP_NAME -e LDAP_HOST -e LDAP_PORT -e LDAP_BASE_DN \
-e LDAP_BIND_DN -e LDAP_BIND_PW -e LDAP_UID_ATTR \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_ldap_source.rb
environment:
LDAP_NAME: "{{ ORGANIZATION }} svc-db-openldap"
LDAP_HOST: "{{ LDAP.SERVER.DOMAIN }}"
LDAP_PORT: "{{ LDAP.SERVER.PORT }}"
LDAP_BASE_DN: "{{ LDAP.DN.OU.USERS }}"
LDAP_BIND_DN: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
LDAP_BIND_PW: "{{ LDAP.BIND_CREDENTIAL }}"
LDAP_UID_ATTR: "{{ LDAP.USER.ATTRIBUTES.ID }}"
register: zammad_ldap_apply
changed_when: false
when: ZAMMAD_LDAP_ENABLED
- name: "configure Zammad's Email channel (inbound IMAP + outbound SMTP) against Mailu"
ansible.builtin.shell: |
set -euo pipefail
container exec -i \
-e MAIL_GROUP_NAME -e MAIL_FROM_NAME -e MAIL_FROM_ADDRESS \
-e IMAP_HOST -e IMAP_PORT -e IMAP_USER -e IMAP_PASS \
-e SMTP_HOST -e SMTP_PORT -e SMTP_USER -e SMTP_PASS \
{{ ZAMMAD_RAILS_CONTAINER }} \
bundle exec rails r - < {{ role_path }}/files/scripts/apply_email_channel.rb
environment:
MAIL_GROUP_NAME: "Users"
MAIL_FROM_NAME: "{{ ORGANIZATION }} Helpdesk"
MAIL_FROM_ADDRESS: "{{ ZAMMAD_HELPDESK_EMAIL }}"
IMAP_HOST: "{{ ZAMMAD_MAIL.host }}"
IMAP_PORT: "143"
IMAP_USER: "{{ ZAMMAD_HELPDESK_EMAIL }}"
IMAP_PASS: "{{ ZAMMAD_HELPDESK_PASS }}"
SMTP_HOST: "{{ ZAMMAD_MAIL.host }}"
SMTP_PORT: "{{ ZAMMAD_MAIL.port }}"
SMTP_USER: "{{ ZAMMAD_HELPDESK_EMAIL }}"
SMTP_PASS: "{{ ZAMMAD_HELPDESK_PASS }}"
register: zammad_email_apply
changed_when: false
when: ZAMMAD_EMAIL_ENABLED and (ZAMMAD_HELPDESK_EMAIL | length > 0)