01_manager_ops.ymlΒΆ

---
- 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: "πŸ” 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: IS_SWARM_MODE

- 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: "πŸ—„οΈ point Zammad's attachment store at the S3 bucket"
  ansible.builtin.shell:
    cmd: |
      set -euo pipefail
      container exec -i \
        {{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} \
        bundle exec rails r -
    stdin: "{{ lookup('file', 'ruby/apply/storage_provider.rb') }}"
  register: zammad_storage_provider_apply
  changed_when: false
  when: ZAMMAD_OBJSTORE_ENABLED | bool

- 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 platform administrator in Zammad while sso is off"
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
  ansible.builtin.shell:
    cmd: |
      set -euo pipefail
      container exec -i \
        -e ADMIN_LOGIN -e ADMIN_EMAIL -e ADMIN_FIRSTNAME -e ADMIN_LASTNAME -e ADMIN_PASSWORD \
        {{ ZAMMAD_RAILS_CONTAINER_ADDRESS }} \
        bundle exec rails r -
    stdin: "{{ lookup('file', 'ruby/apply/administrator.rb') }}"
  environment:
    ADMIN_LOGIN: "{{ ZAMMAD_ADMINISTRATOR.username }}"
    ADMIN_EMAIL: "{{ ZAMMAD_ADMINISTRATOR.email }}"
    ADMIN_FIRSTNAME: "{{ ZAMMAD_ADMINISTRATOR.firstname }}"
    ADMIN_LASTNAME: "{{ ZAMMAD_ADMINISTRATOR.lastname }}"
    ADMIN_PASSWORD: "{{ ZAMMAD_ADMINISTRATOR.password }}"
  register: zammad_administrator_apply
  changed_when: "'administrator: changed' in zammad_administrator_apply.stdout"
  when: not (ZAMMAD_OIDC_ENABLED | bool)

- name: "🎨 Converge the corporate design of {{ application_id }}"
  ansible.builtin.include_tasks: 02_design.yml

- 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: "πŸ€– Wire Zammad Smart Assist to the platform LLM gateway"
  when: ZAMMAD_LITELLM_ENABLED | bool
  block:
    - name: "πŸ” Verify the gateway contract for {{ application_id }}"
      ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-ai-litellm/tasks/utils/consumer_contract.yml') }}"

    - 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 }}"

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