store_guard.ymlΒΆ

---
# Stalwart 1.x keeps a bytea column v in table u; every 0.x store keeps its bigint quota counters under the same name.
# mask_values scrubs the role's names out of psql errors, so existence comes from the shared catalog or the dedicated volume, and a stopped dedicated database fails its own assert instead of a masked psql error.
# The host marker outranks the store: a marked host that goes back to a 0.16 store (rollback, old backup) passes unless the marker is removed first.

- name: "πŸ” Find the 1.x store marker of this host"
  ansible.builtin.stat:
    path: "{{ STALWART_STORE_MARKER_FILE }}"
  register: stalwart_store_marker_file

- name: "πŸ“– Read the 1.x store marker of this host"
  ansible.builtin.slurp:
    src: "{{ STALWART_STORE_MARKER_FILE }}"
  register: stalwart_store_marker
  when: stalwart_store_marker_file.stat.exists

- name: "πŸ”§ Record whether this host last deployed Stalwart 1.x on its store"
  ansible.builtin.set_fact:
    stalwart_store_marked: >-
      {{ stalwart_store_marker is not skipped
         and (stalwart_store_marker.content | b64decode | trim) == (STALWART_STORE_LAYOUT_VERSION | string) }}

- name: "πŸ” Find the store database on the shared PostgreSQL"
  database_query:
    config: >-
      {{ lookup('database', application_id) | combine({
        'username': 'postgres',
        'name': 'postgres',
        'password': lookup('config', 'svc-db-postgres', 'secrets.credentials.POSTGRES_PASSWORD'),
      }) }}
    query_file: "{{ role_path }}/files/sql/store/database.sql"
    named_args:
      name: "{{ lookup('database', application_id, 'name') }}"
    expect_rows: true
    mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
  register: stalwart_store_database
  until: stalwart_store_database is succeeded
  retries: 6
  delay: 5
  changed_when: false
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
  when:
    - not (stalwart_store_marked | bool)
    - STALWART_DATABASE_SHARED | bool

- name: "πŸ” Find the volume of the dedicated store database"
  ansible.builtin.command: >-
    container volume ls -q --filter name=^{{ lookup('database', application_id, 'volume') }}$
  register: stalwart_store_volume
  changed_when: false
  when:
    - not (stalwart_store_marked | bool)
    - not (STALWART_DATABASE_SHARED | bool)

- name: "πŸ”§ Record whether a store exists that this host never deployed Stalwart 1.x on"
  ansible.builtin.set_fact:
    stalwart_store_exists: >-
      {{ not (stalwart_store_marked | bool) and (
           (stalwart_store_database.rows[0][0] | int > 0)
           if STALWART_DATABASE_SHARED | bool
           else (stalwart_store_volume.stdout | trim | length > 0)) }}

- name: "πŸ” Find the running container of the dedicated store database"
  ansible.builtin.command:
    argv:
      - container
      - ps
      - --quiet
      - --filter
      - status=running
      - --filter
      - >-
        {{ ('label=com.docker.swarm.service.name=' ~ lookup('database', application_id, 'service_name'))
           if DEPLOYMENT_MODE == 'swarm'
           else ('name=^' ~ lookup('database', application_id, 'container') ~ '$') }}
  register: stalwart_store_engine
  changed_when: false
  when:
    - stalwart_store_exists | bool
    - not (STALWART_DATABASE_SHARED | bool)

- name: "βœ… Ensure the dedicated store database runs, so the deploy can read its layout"
  ansible.builtin.assert:
    that:
      - stalwart_store_engine.stdout | trim | length > 0
    fail_msg: >-
      The dedicated store database of Stalwart, service '{{ lookup('database', application_id, 'host') }}'
      of stack '{{ application_id | get_entity_name }}', is not running on {{ inventory_hostname }},
      so the deploy cannot read the layout of the store in volume '{{ lookup('database', application_id, 'volume') }}'.
      This host has no record of a Stalwart 1.x deploy ({{ STALWART_STORE_MARKER_FILE }}), so that store may predate 1.0,
      and the deploy stops before Stalwart {{ lookup('config', application_id, 'services.stalwart.version') }} opens it.
      Start that database from the stack this host last deployed, or finish the stalwart-migrate steps of
      "Upgrading from 0.16" in roles/web-app-stalwart/README.md, then deploy again.
    quiet: true
  when: stalwart_store_engine is not skipped

- name: "πŸ” Read the layout of the store Stalwart opens"
  database_query:
    config: "{{ lookup('database', application_id) }}"
    query_file: "{{ role_path }}/files/sql/store/layout.sql"
    expect_rows: true
    mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
  register: stalwart_store_layout
  until: stalwart_store_layout is succeeded
  retries: 6
  delay: 5
  changed_when: false
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
  when: stalwart_store_exists | bool

- name: "βœ… Ensure Stalwart opens no store from before 1.0"
  ansible.builtin.assert:
    that:
      - stalwart_store_layout.rows[0][0] in ['bytea', 'absent']
    fail_msg: >-
      The store in database '{{ lookup('database', application_id, 'name') }}' predates Stalwart 1.0
      (type of column u.v: {{ stalwart_store_layout.rows[0][0] }}, a 1.x store holds bytea).
      Stalwart {{ lookup('config', application_id, 'services.stalwart.version') }} refuses to open it and has no in-place upgrade,
      so the deploy stops before it starts Stalwart. Migrate the store into a new
      database with stalwart-migrate and swap it in as '{{ lookup('database', application_id, 'name') }}',
      following "Upgrading from 0.16" in roles/web-app-stalwart/README.md, then deploy again.
    quiet: true
  when: stalwart_store_layout is not skipped