--- - 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)