Files
computer-playbook/roles/web-app-zammad/tasks/01_manager_ops.yml
kevinveenbirkenbachandClaude Opus 5.5 9ce68b0334 fix(web-app-zammad): wait for the current init task to complete in swarm
In swarm the role gated init only through "wait for Zammad init (seeds
+ pk-sequence reset)", which checks database state. On an existing
installation that state is already there, so a swarm deploy that
restarts init while Elasticsearch refuses shards passed: the init task
kept failing on the allocation precheck and nothing reported it.

A swarm-only task now runs sys-svc-compose's job_state.sh against the
init service after the seeds wait. It passes once the newest init task
is Complete. If the newest task stays Failed, it fails the deploy
after 60 retries 5 s apart. An init the stack deploy did not restart
is already Complete and passes at once. Compose needs no extra wait,
because railsserver depends on init with
service_completed_successfully.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 12:33:54 +02:00

220 lines
8.6 KiB
YAML

---
- name: "⏳ Transferring search_allocation_precheck.sh to {{ ZAMMAD_SEARCH_PRECHECK_ABS }}"
stack_host_copy:
src: shell/search_allocation_precheck.sh
dest: "{{ ZAMMAD_SEARCH_PRECHECK_ABS }}"
mode: "0555"
notify: compose-up
- name: "🎬 Stage Zammad host-side init files (local + swarm peers)"
ansible.builtin.include_tasks:
file: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml') }}"
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"
condition: "{{ ZAMMAD_OIDC_ENABLED }}"
- src: "zz/oidc/http_discovery.rb.j2"
dest: "{{ ZAMMAD_OIDC_HTTP_DISCOVERY_HOST_PATH }}"
mode: "0644"
condition: "{{ ZAMMAD_OIDC_ENABLED and ZAMMAD_HTTP_ONLY_ENABLED }}"
- name: "🔐 grant CREATEDB to Zammad PostgreSQL role (init runs rake db:create)"
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', 'secrets.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 Zammad init starts with auto_wizard.json bind-mounted"
meta: flush_handlers
- name: "⏳ wait for Zammad init (seeds + pk-sequence reset) to finish"
ansible.builtin.shell: |
set -eo pipefail
container exec {{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} \
bundle exec rails r '
raise unless Setting.any?
raise unless ActiveRecord::Base.connection.select_value("SELECT is_called FROM users_id_seq")
'
register: zammad_rails_ready
changed_when: false
retries: 60
delay: 5
until: zammad_rails_ready.rc == 0
- name: "⏳ wait for the current Zammad init task to complete"
ansible.builtin.script: "{{ lookup('path_absolute', 'roles/sys-svc-compose/files/shell/swarm/job_state.sh') }}"
environment:
JOB: "{{ lookup('container_service', application_id, ZAMMAD_INIT_SERVICE) }}"
register: zammad_init_state
changed_when: false
retries: 60
delay: 5
until: zammad_init_state.rc != 1
failed_when: zammad_init_state.rc != 0
when: DEPLOYMENT_MODE == 'swarm'
- name: "▶️ process auto_wizard.json if Zammad setup wizard incomplete (idempotent)"
ansible.builtin.shell: |
set -euo pipefail
container exec -i {{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} 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; all variants, no-auth too)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
ansible.builtin.shell:
cmd: |
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_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/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: "🔑 ensure the read-only MCP agent and its API token in Zammad"
ansible.builtin.shell:
cmd: |
set -euo pipefail
container exec -i \
-e MCP_BOT_LOGIN -e MCP_BOT_EMAIL -e MCP_BOT_FIRSTNAME -e MCP_BOT_LASTNAME \
-e MCP_BOT_PASSWORD -e MCP_API_TOKEN -e MCP_TOKEN_LABEL \
{{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/apply/mcp_bot.rb') }}"
environment:
MCP_BOT_LOGIN: "{{ ZAMMAD_MCP_UPSTREAM_USER }}"
MCP_BOT_EMAIL: "{{ ZAMMAD_MCP_UPSTREAM_EMAIL }}"
MCP_BOT_FIRSTNAME: "MCP"
MCP_BOT_LASTNAME: "Adapter"
MCP_BOT_PASSWORD: "{{ ZAMMAD_MCP_UPSTREAM_PASSWORD }}"
MCP_API_TOKEN: "{{ ZAMMAD_MCP_UPSTREAM_KEY }}"
MCP_TOKEN_LABEL: "{{ ZAMMAD_MCP_UPSTREAM_TOKEN_LABEL }}"
register: zammad_mcp_bot_apply
changed_when: false
when: ZAMMAD_MCP_ENABLED | bool
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "📌 apply OIDC settings in Zammad (post-bootstrap; idempotent)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
ansible.builtin.shell:
cmd: |
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_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/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 LdapSource vs svc-db-openldap (post-bootstrap; idempotent)"
ansible.builtin.shell:
cmd: |
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_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/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: "🔐 Verify the gateway contract for {{ application_id }}"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-ai-litellm/tasks/utils/consumer_contract.yml') }}"
when: ZAMMAD_LITELLM_ENABLED | bool
- name: "🤖 Point Zammad Smart Assist at the platform gateway"
ansible.builtin.shell:
cmd: |
set -euo pipefail
container exec -i \
-e AI_URL -e AI_MODEL -e AI_TOKEN \
{{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/apply/ai_provider.rb') }}"
environment:
AI_URL: "{{ ZAMMAD_LITELLM_BASE_URL }}"
AI_MODEL: "{{ ZAMMAD_LITELLM_MODEL }}"
AI_TOKEN: "{{ ZAMMAD_LITELLM_API_KEY }}"
register: zammad_ai_apply
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
when: ZAMMAD_LITELLM_ENABLED | bool
- name: "🔧 configure Zammad's Email channel (inbound IMAP + outbound SMTP) against Mailu"
ansible.builtin.shell:
cmd: |
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_ADDRESS }} \
bundle exec rails r -
stdin: "{{ lookup('file', 'ruby/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)