mirror of
https://github.com/spantaleev/matrix-docker-ansible-deploy.git
synced 2026-09-12 02:43:14 +00:00
`group_vars/matrix_servers` selects postgres whenever postgres is enabled, which is the default, so postgres is what essentially every deployment runs. The scenarios were testing sqlite - a path almost nobody is on. How little that path is used is not a guess: the mautrix-meta bridges could not start at all under sqlite, and nobody reported it. Testing the engine users are actually on is worth more than keeping coverage of the one they are not, so no scenario is left behind on sqlite. Four of the eight scenarios have a database and are converted; the other four have none and are untouched. molecule-shared/tasks/postgres.yml stands Postgres up on the scenario's network, with the data directory on a tmpfs since it is thrown away with the container. The image is pinned at the major the postgres role deploys to new installations and left to Renovate: when a new major lands, the PR bumping that pin runs every scenario against it, which is the earliest warning we get that a component does not cope. Each scenario gives its database and user names that differ from the role's defaults, so the component reaching the database proves the role built its connection string out of them. The assertions moved from "a file appeared at the path we configured" to "these tables exist", which is strictly stronger: tables can only appear once the component has resolved the hostname, authenticated with the credentials the role rendered, and run its migrations to completion. Costs about 10 seconds per affected scenario (115s to 125s locally for mautrix-whatsapp), on jobs that run in parallel. Gotcha worth recording: since Postgres 18 the image puts PGDATA in a versioned subdirectory and refuses to start if it finds a mount at the old /var/lib/postgresql/data, so the tmpfs is mounted at /var/lib/postgresql. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGT
243 lines
13 KiB
YAML
243 lines
13 KiB
YAML
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
---
|
|
# Proves the bridge starts, reads the configuration and registration the role rendered, opens
|
|
# its appservice port, is the version the role pins, and composed the avatar-proxy labels out
|
|
# of the hostname, scheme and path prefix it was given.
|
|
#
|
|
# It does NOT bridge anything: there is no Discord on the other side, and deliberately never
|
|
# will be, because that would need a Discord account. See docs/molecule-testing.md.
|
|
- name: Verify mautrix-discord
|
|
hosts: all
|
|
become: true
|
|
vars_files:
|
|
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
|
|
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
|
|
gather_facts: false
|
|
|
|
vars:
|
|
# The role derives this from scheme + hostname + path prefix. Role defaults are out of
|
|
# scope in this play, so it is recomposed from the same three values molecule.yml pinned.
|
|
mautrix_discord_expected_public_address: >-
|
|
{{ matrix_bridge_mautrix_discord_scheme }}://{{ matrix_bridge_mautrix_discord_hostname }}{{ matrix_bridge_mautrix_discord_path_prefix }}
|
|
mautrix_discord_expected_avatar_proxy_path_prefix: "{{ matrix_bridge_mautrix_discord_path_prefix }}/mautrix-discord/avatar"
|
|
|
|
tasks:
|
|
# From the role's own defaults rather than pinned in molecule.yml, so the version
|
|
# assertion compares the running image against what the role ships, not the scenario.
|
|
- name: Load the role's defaults under a separate name
|
|
ansible.builtin.include_vars:
|
|
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
|
|
name: mautrix_discord_role_defaults
|
|
|
|
- name: Wait for the mautrix-discord service to become active
|
|
ansible.builtin.systemd_service:
|
|
name: matrix-mautrix-discord.service
|
|
register: mautrix_discord_service
|
|
until: mautrix_discord_service.status.ActiveState == 'active'
|
|
retries: 30
|
|
delay: 5
|
|
failed_when: false
|
|
|
|
# `Restart=always` means a bridge crash-looping on unreadable config still reports
|
|
# `active`, so the restart counter is checked too. Asserted `is defined` because
|
|
# `| int` turns a missing property into 0 and would pass vacuously.
|
|
- name: Assert the service is active and has not been restarting
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_discord_service.status.ActiveState == 'active'
|
|
- mautrix_discord_service.status.NRestarts is defined
|
|
- mautrix_discord_service.status.NRestarts | int == 0
|
|
fail_msg: >-
|
|
matrix-mautrix-discord.service is
|
|
{{ mautrix_discord_service.status.ActiveState | default('unknown') }}
|
|
after {{ mautrix_discord_service.status.NRestarts | default('?') }}
|
|
automatic restart(s)
|
|
success_msg: "matrix-mautrix-discord.service is active and has not restarted"
|
|
|
|
# The appservice listener is where a homeserver would push transactions. It opening at all
|
|
# means the bridge got through reading its configuration and setting itself up.
|
|
- name: Wait for the bridge to open its appservice port
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- run
|
|
- --rm
|
|
- --network={{ matrix_bridge_mautrix_discord_container_network }}
|
|
- "{{ molecule_shared_image_curl }}"
|
|
- --silent
|
|
- --output
|
|
- /dev/null
|
|
- --write-out
|
|
- "HTTP_STATUS=%{http_code}"
|
|
- "http://matrix-mautrix-discord:8080/_matrix/mau/live"
|
|
register: mautrix_discord_live
|
|
changed_when: false
|
|
until: "'HTTP_STATUS=000' not in mautrix_discord_live.stdout"
|
|
retries: 24
|
|
delay: 5
|
|
failed_when: false
|
|
|
|
- name: Assert the bridge answers on its appservice port
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'HTTP_STATUS=000' not in mautrix_discord_live.stdout"
|
|
fail_msg: >-
|
|
The bridge did not answer on its appservice port
|
|
({{ mautrix_discord_live.stdout | default('no output') }})
|
|
success_msg: "The bridge answers on its appservice port"
|
|
|
|
- name: Read the configuration the role rendered
|
|
ansible.builtin.slurp:
|
|
src: "{{ matrix_bridge_mautrix_discord_config_path }}/config.yaml"
|
|
register: mautrix_discord_config_file
|
|
|
|
# Each differs from what the bridge would use on its own, so their presence rules out a
|
|
# coincidence. The public address in particular is composed by the role out of three
|
|
# separate variables.
|
|
#
|
|
# Asserted against the parsed document rather than by substring, so a value landing under
|
|
# the wrong key cannot pass.
|
|
- name: Assert the rendered configuration carries this scenario's values
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_discord_config.homeserver.address == matrix_bridge_mautrix_discord_homeserver_address
|
|
- mautrix_discord_config.homeserver.domain == matrix_bridge_mautrix_discord_homeserver_domain
|
|
- mautrix_discord_config.appservice.bot.username == matrix_bridge_mautrix_discord_appservice_bot_username
|
|
- mautrix_discord_config.appservice.as_token == matrix_bridge_mautrix_discord_appservice_token
|
|
- mautrix_discord_config.appservice.hs_token == matrix_bridge_mautrix_discord_homeserver_token
|
|
- mautrix_discord_config.appservice.database.type == 'postgres'
|
|
- mautrix_discord_config.bridge.command_prefix == matrix_bridge_mautrix_discord_bridge_command_prefix
|
|
- mautrix_discord_config.bridge.avatar_proxy_key == matrix_bridge_mautrix_discord_bridge_avatar_proxy_key
|
|
- mautrix_discord_config.bridge.public_address == mautrix_discord_expected_public_address | trim
|
|
- mautrix_discord_config.logging.min_level == matrix_bridge_mautrix_discord_logging_level
|
|
fail_msg: "The rendered configuration does not carry the scenario's values"
|
|
success_msg: "The rendered configuration carries the scenario's values"
|
|
vars:
|
|
mautrix_discord_config: "{{ mautrix_discord_config_file.content | b64decode | from_yaml }}"
|
|
|
|
# The role generates the registration; the bridge only consumes it. `sender_localpart`
|
|
# carries the role's own `_bot_` prefixing, not something the bridge would produce.
|
|
- name: Read the appservice registration the role rendered
|
|
ansible.builtin.slurp:
|
|
src: "{{ matrix_bridge_mautrix_discord_config_path }}/registration.yaml"
|
|
register: mautrix_discord_registration_file
|
|
|
|
- name: Assert the registration carries the scenario's tokens and bot user
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_discord_registration.as_token == matrix_bridge_mautrix_discord_appservice_token
|
|
- mautrix_discord_registration.hs_token == matrix_bridge_mautrix_discord_homeserver_token
|
|
- mautrix_discord_registration.sender_localpart == '_bot_' ~ matrix_bridge_mautrix_discord_appservice_bot_username
|
|
- mautrix_discord_registration.url == 'http://matrix-mautrix-discord:8080'
|
|
- mautrix_discord_bot_user_regex in (mautrix_discord_registration.namespaces.users | map(attribute='regex') | list)
|
|
fail_msg: "The appservice registration does not carry the scenario's tokens and bot user"
|
|
success_msg: "The appservice registration carries the scenario's tokens and bot user"
|
|
vars:
|
|
mautrix_discord_registration: "{{ mautrix_discord_registration_file.content | b64decode | from_yaml }}"
|
|
mautrix_discord_bot_user_regex: "^@{{ matrix_bridge_mautrix_discord_appservice_bot_username | regex_escape }}:{{ matrix_bridge_mautrix_discord_homeserver_domain | regex_escape }}$"
|
|
|
|
# Stronger than the file-on-disk check sqlite allowed: the bridge can only have created
|
|
# tables here by resolving the hostname, authenticating with the credentials the role
|
|
# rendered, and running its migrations to completion.
|
|
- name: List the tables the bridge created in Postgres
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- exec
|
|
- matrix-postgres-molecule
|
|
- psql
|
|
- --username={{ matrix_bridge_mautrix_discord_database_username }}
|
|
- --dbname={{ matrix_bridge_mautrix_discord_database_name }}
|
|
- --tuples-only
|
|
- --no-align
|
|
- --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
|
|
register: mautrix_discord_tables
|
|
changed_when: false
|
|
|
|
- name: Assert the bridge migrated its schema into the database the role pointed it at
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_discord_tables.rc == 0
|
|
- "'version' in mautrix_discord_table_names"
|
|
- mautrix_discord_table_names | length > 5
|
|
fail_msg: >-
|
|
The bridge did not create its schema in
|
|
{{ matrix_bridge_mautrix_discord_database_name }}
|
|
(found {{ mautrix_discord_table_names | length }} table(s))
|
|
success_msg: "The bridge migrated its schema into the database the role pointed it at"
|
|
vars:
|
|
mautrix_discord_table_names: "{{ mautrix_discord_tables.stdout_lines | select | list }}"
|
|
|
|
- name: Read the image of the running container
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- container
|
|
- inspect
|
|
- matrix-mautrix-discord
|
|
- --format
|
|
- "{{ '{{' }} .Config.Image {{ '}}' }}"
|
|
register: mautrix_discord_image
|
|
changed_when: false
|
|
|
|
- name: Assert the running container is the version defaults/main.yml pins
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_discord_role_defaults.matrix_bridge_mautrix_discord_version in mautrix_discord_image.stdout
|
|
fail_msg: >-
|
|
The running container is {{ mautrix_discord_image.stdout }}, which does
|
|
not carry the pinned version
|
|
{{ mautrix_discord_role_defaults.matrix_bridge_mautrix_discord_version }}
|
|
success_msg: "The running container is the version defaults/main.yml pins"
|
|
|
|
# The avatar proxy is this role's own reverse-proxy wiring. The labels only appear because
|
|
# a public address was configured, and the role composes their hostname and path prefix
|
|
# rather than copying them from a variable.
|
|
- name: Read the labels the role rendered
|
|
ansible.builtin.slurp:
|
|
src: "{{ matrix_bridge_mautrix_discord_base_path }}/labels"
|
|
register: mautrix_discord_labels
|
|
|
|
- name: Assert the avatar-proxy labels were composed from the scenario's hostname and path prefix
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'traefik.enable=true' in mautrix_discord_labels_rendered"
|
|
- "'traefik.docker.network=' ~ matrix_bridge_mautrix_discord_container_network in mautrix_discord_labels_rendered"
|
|
- "'traefik.http.routers.matrix-mautrix-discord-avatar-proxy.rule=Host(`' ~ matrix_bridge_mautrix_discord_hostname ~ '`) && PathPrefix(`' ~ mautrix_discord_expected_avatar_proxy_path_prefix ~ '`)' in mautrix_discord_labels_rendered"
|
|
- "'traefik.http.middlewares.matrix-mautrix-discord-strip-prefix.stripprefix.prefixes=' ~ matrix_bridge_mautrix_discord_path_prefix in mautrix_discord_labels_rendered"
|
|
fail_msg: >-
|
|
The avatar-proxy labels do not carry the scenario's hostname
|
|
({{ matrix_bridge_mautrix_discord_hostname }}) and path prefix
|
|
({{ mautrix_discord_expected_avatar_proxy_path_prefix }})
|
|
success_msg: "The avatar-proxy labels were composed from the scenario's hostname and path prefix"
|
|
vars:
|
|
mautrix_discord_labels_rendered: "{{ mautrix_discord_labels.content | b64decode }}"
|
|
|
|
# The label file is fed to `docker create --label-file`, so a wrongly rendered label is
|
|
# not cosmetic: it stops the container being created at all. Reading them back off the
|
|
# running container proves Docker accepted them.
|
|
- name: Read the labels Docker attached to the running container
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- container
|
|
- inspect
|
|
- matrix-mautrix-discord
|
|
- --format
|
|
- "{{ '{{' }} index .Config.Labels \"traefik.http.routers.matrix-mautrix-discord-avatar-proxy.rule\" {{ '}}' }}"
|
|
register: mautrix_discord_container_label
|
|
changed_when: false
|
|
|
|
- name: Assert Docker carries the avatar-proxy router label the role rendered
|
|
ansible.builtin.assert:
|
|
that:
|
|
- matrix_bridge_mautrix_discord_hostname in mautrix_discord_container_label.stdout
|
|
- mautrix_discord_expected_avatar_proxy_path_prefix in mautrix_discord_container_label.stdout
|
|
fail_msg: >-
|
|
The running container's avatar-proxy router label is
|
|
"{{ mautrix_discord_container_label.stdout }}"
|
|
success_msg: "The running container carries the avatar-proxy router label the role rendered"
|