mirror of
https://github.com/spantaleev/matrix-docker-ansible-deploy.git
synced 2026-09-14 20:03:13 +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
278 lines
16 KiB
YAML
278 lines
16 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, and is the Messenger-side image at the version the role pins.
|
|
#
|
|
# The extra thing worth proving here is that `..._meta_mode` reaches every place it feeds: one
|
|
# upstream codebase serves several Meta networks, and the mode picks which. The scenario runs
|
|
# `facebook-tor` rather than the role's default, so the assertions can tell the two apart.
|
|
#
|
|
# It does NOT bridge anything: no Facebook or Messenger account is on the other side, and
|
|
# deliberately never will be. See docs/molecule-testing.md.
|
|
- name: Verify mautrix-meta-messenger
|
|
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"
|
|
# Lazily evaluated, so they resolve only in the tasks after the matching slurp.
|
|
# Deliberately not set_fact: the rendered configuration contains the bridge's own Go
|
|
# templates, and a stored fact gets templated again on every lookup, which would try to
|
|
# evaluate those as Jinja.
|
|
vars:
|
|
mautrix_meta_messenger_config: "{{ mautrix_meta_messenger_config_file.content | b64decode | from_yaml }}"
|
|
mautrix_meta_messenger_registration: "{{ mautrix_meta_messenger_registration_file.content | b64decode | from_yaml }}"
|
|
mautrix_meta_messenger_labels_rendered: "{{ mautrix_meta_messenger_labels.content | b64decode }}"
|
|
gather_facts: false
|
|
|
|
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_meta_messenger_role_defaults
|
|
|
|
- name: Wait for the mautrix-meta-messenger service to become active
|
|
ansible.builtin.systemd_service:
|
|
name: matrix-mautrix-meta-messenger.service
|
|
register: mautrix_meta_messenger_service
|
|
until: mautrix_meta_messenger_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_meta_messenger_service.status.ActiveState == 'active'
|
|
- mautrix_meta_messenger_service.status.NRestarts is defined
|
|
- mautrix_meta_messenger_service.status.NRestarts | int == 0
|
|
fail_msg: >-
|
|
matrix-mautrix-meta-messenger.service is
|
|
{{ mautrix_meta_messenger_service.status.ActiveState | default('unknown') }}
|
|
after {{ mautrix_meta_messenger_service.status.NRestarts | default('?') }}
|
|
automatic restart(s)
|
|
success_msg: "matrix-mautrix-meta-messenger.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, through its /whoami check, and
|
|
# through setting itself up.
|
|
- name: Wait for the bridge to open its appservice port
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- run
|
|
- --rm
|
|
- --network={{ matrix_bridge_mautrix_meta_messenger_container_network }}
|
|
- "{{ molecule_shared_image_curl }}"
|
|
- --silent
|
|
- --output
|
|
- /dev/null
|
|
- --write-out
|
|
- "HTTP_STATUS=%{http_code}"
|
|
- "http://matrix-mautrix-meta-messenger:29319/_matrix/mau/live"
|
|
register: mautrix_meta_messenger_live
|
|
changed_when: false
|
|
until: "'HTTP_STATUS=000' not in mautrix_meta_messenger_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_meta_messenger_live.stdout"
|
|
fail_msg: >-
|
|
The bridge did not answer on its appservice port
|
|
({{ mautrix_meta_messenger_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_meta_messenger_config_path }}/config.yaml"
|
|
register: mautrix_meta_messenger_config_file
|
|
|
|
# Each differs from what the bridge would use on its own, so their presence rules out
|
|
# a coincidence.
|
|
- name: Assert the rendered configuration carries this scenario's values
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_config.homeserver.address == matrix_bridge_mautrix_meta_messenger_homeserver_address
|
|
- mautrix_meta_messenger_config.homeserver.domain == matrix_bridge_mautrix_meta_messenger_homeserver_domain
|
|
- mautrix_meta_messenger_config.appservice.bot.username == matrix_bridge_mautrix_meta_messenger_appservice_username
|
|
- mautrix_meta_messenger_config.appservice.as_token == matrix_bridge_mautrix_meta_messenger_appservice_token
|
|
- mautrix_meta_messenger_config.appservice.hs_token == matrix_bridge_mautrix_meta_messenger_homeserver_token
|
|
- mautrix_meta_messenger_config.bridge.command_prefix == matrix_bridge_mautrix_meta_messenger_bridge_command_prefix
|
|
- mautrix_meta_messenger_config.logging.min_level == matrix_bridge_mautrix_meta_messenger_logging_min_level
|
|
# matrix_admin is empty in the shared context, so the only per-domain permission
|
|
# left is the one the role derives from the homeserver domain.
|
|
- mautrix_meta_messenger_config.bridge.permissions[matrix_bridge_mautrix_meta_messenger_homeserver_domain] == 'user'
|
|
# From the shared context rather than this scenario, but the role is what has to
|
|
# carry it into the configuration.
|
|
- mautrix_meta_messenger_config.encryption.allow == matrix_bridges_encryption_enabled
|
|
fail_msg: "The rendered configuration does not carry the scenario's values"
|
|
success_msg: "The rendered configuration carries the scenario's values"
|
|
|
|
# The mode is not written into the configuration as-is: the role expands it into an
|
|
# appservice id, a ghost username prefix, a bot displayname and the bridge's `tor` switch.
|
|
# Each holds a different value under the role's default mode, so together they prove the
|
|
# choice propagated rather than being ignored.
|
|
- name: Assert the configuration reflects the Meta mode the scenario selected
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_config.appservice.id == 'facebook-tor'
|
|
- mautrix_meta_messenger_config.appservice.username_template.startswith('facebook_')
|
|
- not mautrix_meta_messenger_config.appservice.username_template.startswith('messenger_')
|
|
- mautrix_meta_messenger_config.network.tor
|
|
- mautrix_meta_messenger_config.appservice.bot.displayname == 'Facebook-tor bridge bot'
|
|
- matrix_bridge_mautrix_meta_messenger_bridge_displayname_suffix in mautrix_meta_messenger_config.network.displayname_template
|
|
fail_msg: >-
|
|
The rendered configuration does not reflect
|
|
matrix_bridge_mautrix_meta_messenger_meta_mode=facebook-tor
|
|
success_msg: "The rendered configuration reflects the Meta mode the scenario selected"
|
|
|
|
- name: Assert the configuration points the bridge at the database the role derived
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_config.database.type == matrix_bridge_mautrix_meta_messenger_database_engine
|
|
- matrix_bridge_mautrix_meta_messenger_database_username in mautrix_meta_messenger_config.database.uri
|
|
- matrix_bridge_mautrix_meta_messenger_database_name in mautrix_meta_messenger_config.database.uri
|
|
- matrix_bridge_mautrix_meta_messenger_database_hostname in mautrix_meta_messenger_config.database.uri
|
|
fail_msg: >-
|
|
database.uri is {{ mautrix_meta_messenger_config.database.uri | default('unset') }},
|
|
which was not built from the scenario's connection settings
|
|
success_msg: "The rendered configuration points the bridge at the database the role derived"
|
|
|
|
# The public address is what the role builds out of the three exposure
|
|
# variables; it is the same endpoint the Traefik labels below route to.
|
|
- name: Assert the configuration carries the public address the exposure settings imply
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_config.appservice.public_address == 'https://bridges.molecule.local/bridges/meta-messenger'
|
|
fail_msg: >-
|
|
appservice.public_address is
|
|
{{ mautrix_meta_messenger_config.appservice.public_address | default('unset') }}
|
|
success_msg: "The rendered configuration carries the public address the exposure settings imply"
|
|
|
|
# The role generates the registration; the bridge only consumes it. Worth checking on its
|
|
# own, as it is the half of the handshake the homeserver reads.
|
|
- name: Read the appservice registration the role rendered
|
|
ansible.builtin.slurp:
|
|
src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/registration.yaml"
|
|
register: mautrix_meta_messenger_registration_file
|
|
|
|
- name: Assert the registration carries the scenario's tokens, id and bot user
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_registration.as_token == matrix_bridge_mautrix_meta_messenger_appservice_token
|
|
- mautrix_meta_messenger_registration.hs_token == matrix_bridge_mautrix_meta_messenger_homeserver_token
|
|
- mautrix_meta_messenger_registration.id == 'facebook-tor'
|
|
- mautrix_meta_messenger_registration.sender_localpart == '_bot_' + matrix_bridge_mautrix_meta_messenger_appservice_username
|
|
- mautrix_meta_messenger_registration.url == 'http://matrix-mautrix-meta-messenger:29319'
|
|
fail_msg: "The appservice registration does not carry the scenario's tokens, id and bot user"
|
|
success_msg: "The appservice registration carries the scenario's tokens, id and bot user"
|
|
|
|
# The namespaces are regexes the role assembles. Comparing them as strings would only
|
|
# re-derive the role's own escaping, so they are checked by what they match: the bot,
|
|
# this mode's ghosts, and not the ghosts of the mode the role would have defaulted to.
|
|
- name: Assert the registration namespaces cover the bot and this mode's ghost users
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_ghost_regex | length > 0
|
|
- mautrix_meta_messenger_bot_regex | length > 0
|
|
- mautrix_meta_messenger_facebook_ghost_mxid is match(mautrix_meta_messenger_ghost_regex)
|
|
- mautrix_meta_messenger_messenger_ghost_mxid is not match(mautrix_meta_messenger_ghost_regex)
|
|
- mautrix_meta_messenger_bot_mxid is match(mautrix_meta_messenger_bot_regex)
|
|
fail_msg: "The appservice registration namespaces do not cover the bot and this mode's ghost users"
|
|
success_msg: "The appservice registration namespaces cover the bot and this mode's ghost users"
|
|
vars:
|
|
mautrix_meta_messenger_user_regexes: "{{ mautrix_meta_messenger_registration.namespaces.users | map(attribute='regex') | list }}"
|
|
mautrix_meta_messenger_ghost_regex: "{{ mautrix_meta_messenger_user_regexes | select('search', 'facebook_') | first | default('') }}"
|
|
mautrix_meta_messenger_bot_regex: "{{ mautrix_meta_messenger_user_regexes | reject('search', 'facebook_') | first | default('') }}"
|
|
mautrix_meta_messenger_facebook_ghost_mxid: "@facebook_1234567890:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}"
|
|
mautrix_meta_messenger_messenger_ghost_mxid: "@messenger_1234567890:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}"
|
|
mautrix_meta_messenger_bot_mxid: "@{{ matrix_bridge_mautrix_meta_messenger_appservice_username }}:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}"
|
|
|
|
# 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_meta_messenger_database_username }}
|
|
- --dbname={{ matrix_bridge_mautrix_meta_messenger_database_name }}
|
|
- --tuples-only
|
|
- --no-align
|
|
- --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
|
|
register: mautrix_meta_messenger_tables
|
|
changed_when: false
|
|
|
|
- name: Assert the bridge migrated its schema into the database the role pointed it at
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_tables.rc == 0
|
|
- "'version' in mautrix_meta_messenger_table_names"
|
|
- mautrix_meta_messenger_table_names | length > 5
|
|
fail_msg: >-
|
|
The bridge did not create its schema in
|
|
{{ matrix_bridge_mautrix_meta_messenger_database_name }}
|
|
(found {{ mautrix_meta_messenger_table_names | length }} table(s))
|
|
success_msg: "The bridge migrated its schema into the database the role pointed it at"
|
|
vars:
|
|
mautrix_meta_messenger_table_names: "{{ mautrix_meta_messenger_tables.stdout_lines | select | list }}"
|
|
|
|
- name: Read the image of the running container
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- container
|
|
- inspect
|
|
- matrix-mautrix-meta-messenger
|
|
- --format
|
|
- "{{ '{{' }} .Config.Image {{ '}}' }}"
|
|
register: mautrix_meta_messenger_image
|
|
changed_when: false
|
|
|
|
# Both Meta bridges publish to the same image repository, with Instagram's tags carrying
|
|
# an `ig-` prefix. Compared whole rather than by substring, so pulling the other bridge's
|
|
# image would fail here.
|
|
- name: Assert the running container is the Messenger image at the version defaults/main.yml pins
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mautrix_meta_messenger_image.stdout.endswith(':' + mautrix_meta_messenger_role_defaults.matrix_bridge_mautrix_meta_messenger_version)
|
|
fail_msg: >-
|
|
The running container is {{ mautrix_meta_messenger_image.stdout }}, which is not
|
|
the Messenger image at the pinned version
|
|
{{ mautrix_meta_messenger_role_defaults.matrix_bridge_mautrix_meta_messenger_version }}
|
|
success_msg: "The running container is the Messenger image at the version defaults/main.yml pins"
|
|
|
|
- name: Read the labels the role rendered
|
|
ansible.builtin.slurp:
|
|
src: "{{ matrix_bridge_mautrix_meta_messenger_base_path }}/labels"
|
|
register: mautrix_meta_messenger_labels
|
|
|
|
# Traefik is not running here, so this checks what the role wrote, not what a reverse
|
|
# proxy would do with it. The port matters: it is hardcoded in the role's templates rather
|
|
# than derived from a variable, so nothing else here would catch it drifting from the port
|
|
# the bridge actually listens on.
|
|
- name: Assert the labels route the exposure hostname and path prefix to the appservice port
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'traefik.enable=true' in mautrix_meta_messenger_labels_rendered"
|
|
- "'traefik.http.services.matrix-mautrix-meta-messenger-appservice.loadbalancer.server.port=29319' in mautrix_meta_messenger_labels_rendered"
|
|
- "'traefik.http.routers.matrix-mautrix-meta-messenger-exposure.rule=Host(`bridges.molecule.local`) && PathPrefix(`/bridges/meta-messenger`)' in mautrix_meta_messenger_labels_rendered"
|
|
- "'traefik.http.middlewares.matrix-mautrix-meta-messenger-exposure-strip-prefix.stripprefix.prefixes=/bridges/meta-messenger' in mautrix_meta_messenger_labels_rendered"
|
|
- "'traefik.docker.network=' + matrix_bridge_mautrix_meta_messenger_container_network in mautrix_meta_messenger_labels_rendered"
|
|
fail_msg: "The rendered labels do not route the exposure hostname and path prefix to the appservice port"
|
|
success_msg: "The rendered labels route the exposure hostname and path prefix to the appservice port"
|