fix(matrix): give each bridge declared tokens and a rendered registration, drop the swarm service removal

Every mautrix bridge generated its own registration with -g on start,
with fresh random tokens. Synapse had to read that file from a shared
directory, be restarted afterwards, and on swarm the services had to be
removed first so the registrations could be wiped (93735fd533). The
tokens changed on every redeploy and the deploy depended on a wait for
files a container writes.

Each bridge now has as_token and hs_token declared in meta/secrets.yml
and a fixed appservice_port in its addon. The bridge config reads the
tokens through lookup('config'), and registration.yaml.j2 renders the
registration from that config in the upstream -g format, so both sides
are known before anything starts and stay identical across redeploys.

The files reach the containers as secrets: matrix_bridge_mounts offers a
stack view (top-level definitions, content-hash named by compose_volumes),
a synapse view (one registration per bridge under /registrations, mode
0444) and a bridge view (the config at /config/config.yaml). The bridges
run as the synapse uid with an explicit --config and --no-update, keep
their scratch /data on a 16m tmpfs so hlth-volumes finds no anonymous
volume, and depend on a healthy synapse. Their healthcheck is
/_matrix/mau/ready, which the bridge only reports after whoami with the
as_token and an appservice ping carrying the hs_token, so a healthy
bridge proves the token pair.

The compose tasks lose the compose down, the swarm service rm block, the
registration wait, the permission fixes and both synapse restarts; this
reverts the workaround of 93735fd533.

Verified on the local compose stack: four bridges ready, synapse pings
answered, registration and config sha256 unchanged across a redeploy,
test-bridge-roster and 12 matrix Playwright specs green. A token
rotation (new pair in both files, compose up --force-recreate as the
handler runs it) came back healthy; a mismatched pair left the bridge
unhealthy. The swarm-shaped stack file passes docker stack config with
the 0444 secret modes, the 16 MiB tmpfs and content-hash secret names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-11 11:43:16 +02:00
1 parent a650801b35
commit 304144ce67
20 files changed
+280 -196

No files matched your search

+5 -6
View File
@@ -167,10 +167,8 @@ flowchart TD
end
kc["keycloak_keycloak<br/>+ one-shot realm import job"]
role["web-app-matrix tasks"] -- "renders compose.yml,<br/>notify: swarm deploy" --> flush["meta: flush_handlers<br/>(before registration wait)"]
flush -- "docker stack deploy" --> stack
stack -- "writes mautrix/*/registration.yaml" --> role
role -- "waits for registration files" --> stack
role["web-app-matrix tasks"] -- "renders config/mautrix/*/{config,registration}.yaml<br/>and compose.yml, notify: swarm deploy" --> flush["meta: flush_handlers<br/>(before the Synapse wait)"]
flush -- "docker stack deploy<br/>(both files as secrets)" --> stack
syn -- "OIDC discovery via<br/>--add-host issuer → subnet .1" --> prx
prx -- "vhost auth.*" --> kc
@@ -190,8 +188,9 @@ The ansible flavor maps each true flag to the matching `matrix_<bridge>_enabled`
The mautrix network bridges are declared in
[`meta/addons/`](./meta/addons/) as `mechanism: bridge` addons
(requirement 026, Decision 13). Each is `required: false` and **disabled by default**; its
per-network DB password is referenced from
[`meta/secrets.yml`](./meta/secrets.yml) `credentials:`, never inlined.
per-network DB password and its appservice `as_token`/`hs_token` pair are referenced from
[`meta/secrets.yml`](./meta/secrets.yml) `credentials:`, never inlined. The role renders each
bridge's `config.yaml` and the matching Synapse `registration.yaml` from the same tokens.
| Addon | Mechanism | Default state | Bridges |
|-------|-----------|---------------|---------|
@@ -1,25 +1,42 @@
"""Lookup ``matrix_bridge_mounts``: the synapse registration-file mounts,
one ``<instance>/mautrix/<bridge>:<registration-folder>/mautrix-<bridge>:ro``
short-form entry per enabled bridge.
"""Lookup ``matrix_bridge_mounts``: the secrets that carry each enabled
bridge's config and registration.
Usage (compose template):
Usage (compose templates):
extra_volumes=lookup('matrix_bridge_mounts')
lookup('compose_volumes', extra_secrets=lookup('matrix_bridge_mounts', 'stack'))
lookup('container_volumes', 'synapse', extra_secrets=lookup('matrix_bridge_mounts', 'synapse'))
lookup('container_volumes', service_name, extra_secrets=lookup('matrix_bridge_mounts', 'bridge', item.bridge_name))
Terms:
stack -- top-level secret definitions, named by compose_volumes
synapse -- one registration mount per bridge
bridge <name> -- the config mount of that bridge
Reads from the templating context:
application_id -- the consuming role
MATRIX_BRIDGES -- enabled bridge configs (set_fact)
MATRIX_BRIDGE_SOURCE_DIR -- host directory holding <bridge>/*.yaml
MATRIX_BRIDGE_CONFIG_TARGET -- config path inside each bridge
MATRIX_REGISTRATION_FILE_FOLDER -- registration folder inside synapse
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
SECRET_MODE = 0o444
def _config_key(bridge: str) -> str:
return f"mautrix_{bridge}_config"
def _registration_key(bridge: str) -> str:
return f"mautrix_{bridge}_registration"
class LookupModule(LookupBase):
def run(
@@ -27,9 +44,13 @@ class LookupModule(LookupBase):
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[list[str]]:
if terms:
raise AnsibleError("matrix_bridge_mounts lookup expects no terms.")
) -> list[Any]:
view = str(terms[0]) if terms else ""
if view not in ("stack", "synapse", "bridge"):
raise AnsibleError(
"matrix_bridge_mounts lookup expects 'stack', 'synapse' or "
f"'bridge <name>', got {terms!r}"
)
vars_ = variables or getattr(self._templar, "available_variables", {}) or {}
templar = getattr(self, "_templar", None)
@@ -45,23 +66,49 @@ class LookupModule(LookupBase):
value = templar.template(value)
return value
application_id = str(ctx("application_id")).strip()
bridges = ctx("MATRIX_BRIDGES")
reg_folder = str(ctx("MATRIX_REGISTRATION_FILE_FOLDER"))
if not isinstance(bridges, list):
raise AnsibleError(
"matrix_bridge_mounts lookup: MATRIX_BRIDGES must be a list, "
f"got {type(bridges).__name__}"
)
names = [str(bridge["bridge_name"]) for bridge in bridges]
instance_dir = str(
lookup_loader.get("container", loader=self._loader, templar=templar).run(
[application_id, "directories.instance"], variables=vars_
)[0]
)
if view == "bridge":
if len(terms) != 2 or str(terms[1]) not in names:
raise AnsibleError(
f"matrix_bridge_mounts lookup: 'bridge' needs one enabled bridge name, got {terms[1:]!r}"
)
return [
[
{
"source": _config_key(str(terms[1])),
"target": str(ctx("MATRIX_BRIDGE_CONFIG_TARGET")),
"mode": SECRET_MODE,
}
]
]
mounts = []
for bridge in bridges:
name = str(bridge["bridge_name"])
mounts.append(f"{instance_dir}mautrix/{name}:{reg_folder}mautrix-{name}:ro")
return [mounts]
if view == "synapse":
folder = str(ctx("MATRIX_REGISTRATION_FILE_FOLDER"))
return [
[
{
"source": _registration_key(name),
"target": f"{folder}mautrix-{name}/registration.yaml",
"mode": SECRET_MODE,
}
for name in names
]
]
source_dir = Path(str(ctx("MATRIX_BRIDGE_SOURCE_DIR")))
secrets: dict[str, dict[str, str]] = {}
for name in names:
secrets[_config_key(name)] = {
"file": str(source_dir / name / "config.yaml")
}
secrets[_registration_key(name)] = {
"file": str(source_dir / name / "registration.yaml")
}
return [secrets]
@@ -9,6 +9,7 @@ update:
upstream_id: mautrix-meta
config:
bridge_name: "meta"
appservice_port: 29319
database_username: "mautrix_meta_bridge"
database_name: "mautrix_meta_bridge"
database_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.mautrix_meta_bridge_database_password', '') }}"
@@ -9,6 +9,7 @@ update:
upstream_id: mautrix-signal
config:
bridge_name: "signal"
appservice_port: 29328
database_username: "mautrix_signal_bridge"
database_name: "mautrix_signal_bridge"
database_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.mautrix_signal_bridge_database_password', '') }}"
@@ -9,6 +9,7 @@ update:
upstream_id: mautrix-slack
config:
bridge_name: "slack"
appservice_port: 29335
database_username: "mautrix_slack_bridge"
database_name: "mautrix_slack_bridge"
database_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.mautrix_slack_bridge_database_password', '') }}"
@@ -9,6 +9,7 @@ update:
upstream_id: mautrix-telegram
config:
bridge_name: "telegram"
appservice_port: 29317
database_username: "mautrix_telegram_bridge"
database_name: "mautrix_telegram_bridge"
database_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.mautrix_telegram_bridge_database_password', '') }}"
@@ -9,6 +9,7 @@ update:
upstream_id: mautrix-whatsapp
config:
bridge_name: "whatsapp"
appservice_port: 29318
database_username: "mautrix_whatsapp_bridge"
database_name: "mautrix_whatsapp_bridge"
database_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.mautrix_whatsapp_bridge_database_password', '') }}"
+40
View File
@@ -46,6 +46,46 @@ credentials:
description: Database password for the mautrix-meta bridge (Facebook Messenger + Instagram)
algorithm: bcrypt
validation: ^\$2[aby]\$.{56}$
mautrix_meta_bridge_as_token:
description: Appservice token the mautrix-meta bridge presents to Synapse
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_meta_bridge_hs_token:
description: Token Synapse presents to the mautrix-meta bridge
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_signal_bridge_as_token:
description: Appservice token the mautrix-signal bridge presents to Synapse
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_signal_bridge_hs_token:
description: Token Synapse presents to the mautrix-signal bridge
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_slack_bridge_as_token:
description: Appservice token the mautrix-slack bridge presents to Synapse
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_slack_bridge_hs_token:
description: Token Synapse presents to the mautrix-slack bridge
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_telegram_bridge_as_token:
description: Appservice token the mautrix-telegram bridge presents to Synapse
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_telegram_bridge_hs_token:
description: Token Synapse presents to the mautrix-telegram bridge
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_whatsapp_bridge_as_token:
description: Appservice token the mautrix-whatsapp bridge presents to Synapse
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_whatsapp_bridge_hs_token:
description: Token Synapse presents to the mautrix-whatsapp bridge
algorithm: alphanumeric
validation: ^[A-Za-z0-9]{64}$
mautrix_signal_bridge_database_password:
description: Database password for the mautrix-signal bridge
algorithm: bcrypt
+20 -1
View File
@@ -4,7 +4,7 @@ synapse_data:
name: matrix_synapse_data
mounts:
- service: synapse
target: /data
target: &data_target /data
when: "{{ lookup('config', 'web-app-matrix', 'services.matrix.flavor', 'ansible') == 'compose' }}"
chatgpt_data:
@@ -14,6 +14,25 @@ chatgpt_data:
- service: matrix-chatgpt-bot
target: /storage
mautrix_workdir:
type: tmpfs
mounts:
- service: mautrix-whatsapp
target: *data_target
size: 16m
- service: mautrix-meta
target: *data_target
size: 16m
- service: mautrix-signal
target: *data_target
size: 16m
- service: mautrix-slack
target: *data_target
size: 16m
- service: mautrix-telegram
target: *data_target
size: 16m
synapse_config:
type: config
source: "{{ [ lookup('container', 'web-app-matrix', 'directories.config'), 'homeserver.yaml' ] | path_join }}"
@@ -13,44 +13,9 @@
database_username: "{{ item.database_username }}"
database_name: "{{ item.database_name }}"
loop: "{{ MATRIX_BRIDGES }}"
- name: 🐳 shut down compose project
command:
cmd: "{{ BIN_COMPOSE }} down"
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
when: DEPLOYMENT_MODE == 'compose'
- when:
- DEPLOYMENT_MODE == 'swarm'
- MATRIX_BRIDGES | length > 0
delegate_to: "{{ STACK_HOST }}"
run_once: true
timeout: "{{ lookup('timeout', 60) | int }}"
block:
- name: "🔍 List the services a previous swarm deploy left running"
ansible.builtin.command:
argv:
- container
- service
- ls
- --filter
- "label=com.docker.stack.namespace={{ application_id | get_entity_name }}"
- --format
- "{{ '{{.Name}}' }}"
register: matrix_swarm_services
changed_when: false
- name: "🐝 Remove synapse and the bridges before their registrations are wiped"
ansible.builtin.command:
argv: "{{ ['container', 'service', 'rm'] + matrix_swarm_registration_services }}"
vars:
matrix_swarm_registration_services: >-
{{ ([MATRIX_SYNAPSE_SERVICE]
+ (MATRIX_BRIDGES | map(attribute='bridge_name')
| map('regex_replace', '^', (application_id | get_entity_name) ~ '_mautrix-') | list))
| select('in', matrix_swarm_services.stdout_lines) | list }}
changed_when: true
when: matrix_swarm_registration_services | length > 0
loop_control:
label: "{{ item.bridge_name }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🧹 cleanup project folder"
file:
@@ -59,16 +24,31 @@
- name: "📁 create bridge folders"
file:
path: "{{ lookup('container', application_id, 'directories.instance') }}mautrix/{{ item.bridge_name }}"
path: "{{ [ MATRIX_BRIDGE_SOURCE_DIR, item.bridge_name ] | path_join }}"
state: directory
mode: "0755"
loop: "{{ MATRIX_BRIDGES }}"
loop_control:
label: "{{ item.bridge_name }}"
- name: ➕ add multiple mautrix bridge configuration
stack_host_template:
src: "flavor/compose/mautrix/{{ item.bridge_name }}.config.yml.j2"
dest: "{{ lookup('container', application_id, 'directories.instance') }}mautrix/{{ item.bridge_name }}/config.yaml"
dest: "{{ [ MATRIX_BRIDGE_SOURCE_DIR, item.bridge_name, 'config.yaml' ] | path_join }}"
loop: "{{ MATRIX_BRIDGES }}"
loop_control:
label: "{{ item.bridge_name }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
notify: compose-up
- name: "➕ add the registration each bridge config implies"
stack_host_template:
src: "flavor/compose/mautrix/registration.yaml.j2"
dest: "{{ [ MATRIX_BRIDGE_SOURCE_DIR, item.bridge_name, 'registration.yaml' ] | path_join }}"
loop: "{{ MATRIX_BRIDGES }}"
loop_control:
label: "{{ item.bridge_name }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
notify: compose-up
- name: ➕ add element configuration
@@ -104,47 +84,9 @@
include_tasks: 03_compose_pull_and_up.yml
when: DEPLOYMENT_MODE == 'compose'
- name: "🚿 Flush handlers so the swarm stack deploys before the registration wait"
- name: "🚿 Flush handlers so the stack deploys before the Synapse wait"
meta: flush_handlers
- name: ⏳ wait for registration files
wait_for:
path: "{{ lookup('container', application_id, 'directories.instance') }}mautrix/{{ item.bridge_name }}/registration.yaml"
state: present
timeout: "{{ lookup('timeout', 120) | int }}"
loop: "{{ MATRIX_BRIDGES }}"
- name: "🔐 Make each bridge directory traversable for synapse"
file:
path: "{{ lookup('container', application_id, 'directories.instance') }}mautrix/{{ item.bridge_name }}"
state: directory
mode: "0755"
loop: "{{ MATRIX_BRIDGES }}"
- name: "🔐 change file permissions"
file:
path: "{{ lookup('container', application_id, 'directories.instance') }}mautrix/{{ item.bridge_name }}/registration.yaml"
mode: "0755"
loop: "{{ MATRIX_BRIDGES }}"
- name: "🔄 Restart synapse to load bridge registrations (compose mode)"
command:
cmd: "{{ BIN_COMPOSE }} restart synapse"
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
changed_when: true
when:
- MATRIX_BRIDGES | length > 0
- DEPLOYMENT_MODE == 'compose'
- name: "🔄 Restart synapse to load bridge registrations (swarm mode)"
ansible.builtin.command: "container service update --force {{ MATRIX_SYNAPSE_SERVICE }}"
changed_when: true
timeout: "{{ lookup('timeout', 600) | int }}"
when:
- MATRIX_BRIDGES | length > 0
- DEPLOYMENT_MODE == 'swarm'
- IS_STACK_HOST | bool
- name: ⏳ wait for Synapse client/versions endpoint
uri_retry:
url: "http://127.0.0.1:{{ lookup('config', application_id, 'services.synapse.ports.local.http') }}/_matrix/client/versions"
@@ -5,6 +5,6 @@
{% include 'flavor/compose/services.yml.j2' %}
{% endif %}
{{ lookup('compose_volumes') }}
{{ lookup('compose_volumes', extra_secrets=(lookup('matrix_bridge_mounts', 'stack') if matrix_flavor == 'compose' else {})) }}
{{ lookup('compose_networks') }}
@@ -115,10 +115,10 @@ homeserver:
retry_limit: 4
appservice:
address: http://mautrix-meta:29319
address: http://mautrix-meta:{{ item.appservice_port }}
hostname: 0.0.0.0
port: 29319
port: {{ item.appservice_port }}
id: meta
bot:
@@ -129,8 +129,8 @@ appservice:
ephemeral_events: true
async_transactions: false
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
as_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_meta_bridge_as_token') }}"
hs_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_meta_bridge_hs_token') }}"
{% raw %}
username_template: meta_{{.}}
@@ -0,0 +1,20 @@
{% set bridge = lookup('template', lookup('path_absolute', 'roles/web-app-matrix/templates/flavor/compose/mautrix/' ~ item.bridge_name ~ '.config.yml.j2')) | from_yaml %}
{% set appservice = bridge.appservice %}
{% set domain = bridge.homeserver.domain | string | regex_escape %}
{% set ghost = appservice.username_template.split('{{.}}') %}
{{ {
'id': appservice.id,
'url': appservice.address,
'as_token': appservice.as_token,
'hs_token': appservice.hs_token,
'sender_localpart': '_bot_' ~ appservice.bot.username,
'rate_limited': false,
'namespaces': {
'users': [
{'regex': '^@' ~ (appservice.bot.username | regex_escape) ~ ':' ~ domain ~ '$', 'exclusive': true},
{'regex': '^@' ~ (ghost[0] | regex_escape) ~ '.*' ~ (ghost[1] | regex_escape) ~ ':' ~ domain ~ '$', 'exclusive': true},
],
},
'de.sorunome.msc2409.push_ephemeral': appservice.ephemeral_events,
'receive_ephemeral': appservice.ephemeral_events,
} | to_nice_yaml(indent=2) }}
@@ -47,10 +47,10 @@ homeserver:
retry_limit: 4
appservice:
address: http://mautrix-signal:29328
address: http://mautrix-signal:{{ item.appservice_port }}
hostname: 0.0.0.0
port: 29328
port: {{ item.appservice_port }}
id: signal
bot:
@@ -61,8 +61,8 @@ appservice:
ephemeral_events: true
async_transactions: false
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
as_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_signal_bridge_as_token') }}"
hs_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_signal_bridge_hs_token') }}"
{% raw %}
username_template: signal_{{.}}
@@ -125,9 +125,3 @@ logging:
writers:
- type: stdout
format: pretty-colored
- type: file
format: json
filename: ./logs/mautrix-signal.log
max_size: 100
max_backups: 10
compress: true
@@ -21,10 +21,10 @@ homeserver:
retry_limit: 4
appservice:
address: http://mautrix-slack:29335
address: http://mautrix-slack:{{ item.appservice_port }}
hostname: 0.0.0.0
port: 29335
port: {{ item.appservice_port }}
id: slack
bot:
@@ -35,8 +35,8 @@ appservice:
ephemeral_events: true
async_transactions: false
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
as_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_slack_bridge_as_token') }}"
hs_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_slack_bridge_hs_token') }}"
{% raw %}
username_template: slack_{{.}}
@@ -128,9 +128,3 @@ logging:
writers:
- type: stdout
format: pretty-colored
- type: file
format: json
filename: ./logs/mautrix-slack.log
max_size: 100
max_backups: 10
compress: true
@@ -63,9 +63,9 @@ homeserver:
retry_limit: 4
appservice:
address: http://mautrix-telegram:29317
address: http://mautrix-telegram:{{ item.appservice_port }}
hostname: 0.0.0.0
port: 29317
port: {{ item.appservice_port }}
id: telegram
bot:
@@ -75,8 +75,8 @@ appservice:
ephemeral_events: true
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
as_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_telegram_bridge_as_token') }}"
hs_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_telegram_bridge_hs_token') }}"
username_template: {% raw %}telegram_{{.}}{% endraw %}
@@ -189,9 +189,3 @@ logging:
writers:
- type: stdout
format: pretty-colored
- type: file
format: json
filename: ./logs/bridge.log
max_size: 100
max_backups: 10
compress: false
@@ -138,10 +138,10 @@ homeserver:
retry_limit: 4
appservice:
address: http://mautrix-whatsapp:29318
address: http://mautrix-whatsapp:{{ item.appservice_port }}
hostname: 0.0.0.0
port: 29318
port: {{ item.appservice_port }}
id: whatsapp
bot:
@@ -152,8 +152,8 @@ appservice:
ephemeral_events: true
async_transactions: false
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
as_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_whatsapp_bridge_as_token') }}"
hs_token: "{{ lookup('config', application_id, 'secrets.credentials.mautrix_whatsapp_bridge_hs_token') }}"
{% raw %}
username_template: whatsapp_{{.}}
@@ -7,7 +7,7 @@
{{ lookup('compose_restart') }}
logging:
driver: journald
{{ lookup('container_volumes', service_name, extra_volumes=lookup('matrix_bridge_mounts')) | indent(4) }}
{{ lookup('container_volumes', service_name, extra_secrets=lookup('matrix_bridge_mounts', 'synapse')) | indent(4) }}
environment:
- SYNAPSE_SERVER_NAME={{ MATRIX_SYNAPSE_DOMAIN }}
- SYNAPSE_REPORT_STATS=no
@@ -15,11 +15,7 @@
- "{{ DOCKER_BIND_HOST }}:{{lookup('config', 'web-app-matrix', 'services.synapse.ports.local.http')}}:{{ container_port }}"
{{ lookup('container_extra_hosts') | indent(4) }}
{{ lookup('container_healthcheck', service_name) | indent(4) }}
{% set _synapse_bridge_deps = {} %}
{% for item in MATRIX_BRIDGES %}
{% set _ = _synapse_bridge_deps.__setitem__('mautrix-' ~ item.bridge_name, {'condition': 'service_healthy'}) %}
{% endfor %}
{{ lookup('container_depends_on', application_id, extra=_synapse_bridge_deps) | indent(4) }}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% set service_name = 'element' %}
{{ service_name }}:
@@ -41,19 +37,17 @@
{{ lookup('compose_only', 'container_name', 'matrix-' ~ item.bridge_name) }}
image: "dock.mau.dev/mautrix/{{ item.bridge_name }}:latest" # nocheck: container-image-lookup
{{ lookup('compose_restart') }}
environment:
UID: "{{ MATRIX_SYNAPSE_UID }}"
GID: "{{ MATRIX_SYNAPSE_GID }}"
volumes: # nocheck: compose-inline-mount-section
- ./mautrix/{{ item.bridge_name }}:/data
user: "{{ MATRIX_SYNAPSE_UID }}:{{ MATRIX_SYNAPSE_GID }}"
command: ["/usr/bin/mautrix-{{ item.bridge_name }}", "--config", "{{ MATRIX_BRIDGE_CONFIG_TARGET }}", "--no-update"]
{{ lookup('container_volumes', service_name, extra_secrets=lookup('matrix_bridge_mounts', 'bridge', item.bridge_name)) | indent(4) }}
healthcheck:
test: ["CMD-SHELL", "test -f {{ MATRIX_REGISTRATION_FILE_FOLDER }}registration.yaml || exit 1"]
test: ["CMD", "curl", "--noproxy", "*", "-fsS", "http://127.0.0.1:{{ item.appservice_port }}/_matrix/mau/ready"]
interval: 2m
# nocheck: healthcheck-timing-override the probe waits for a file the registration step writes, and the share it reads answers slower than the 5s default under load
timeout: 10s
retries: 5
timeout: 5s
retries: 3
start_period: 10m
start_interval: 5s
{{ lookup('container_depends_on', application_id, extra={'synapse': {'condition': 'service_healthy'}}) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{% endfor %}
{% if lookup('config', application_id, 'services.matrix.plugins.chatgpt') | bool %}
+3 -1
View File
@@ -12,7 +12,9 @@ MATRIX_ADMINISTRATOR_USERNAME: "{{ lookup('users', 'administrator').username }}"
MATRIX_ADMINISTRATOR_PASSWORD: "{{ lookup('users', 'administrator').password }}"
MATRIX_WELL_KNOWN_DIRECTORY: "{{ [ lookup('nginx','directories.data.well_known'), 'matrix' ] | path_join }}"
MATRIX_WELL_KNOWN_FILE: "{{ MATRIX_WELL_KNOWN_DIRECTORY }}server"
MATRIX_REGISTRATION_FILE_FOLDER: "/data/"
MATRIX_REGISTRATION_FILE_FOLDER: "/registrations/"
MATRIX_BRIDGE_SOURCE_DIR: "{{ [ lookup('container', application_id, 'directories.config'), 'mautrix' ] | path_join }}"
MATRIX_BRIDGE_CONFIG_TARGET: "/config/config.yaml"
MATRIX_REGISTRATION_SHARED_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.registration_shared_secret') }}"
MATRIX_FEDERATION_PORT: "{{ lookup('config', 'web-app-matrix', 'services.synapse.ports.public.federation') }}"
MATRIX_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
@@ -4,7 +4,6 @@ from __future__ import annotations
import importlib.util
import unittest
import unittest.mock as mock
from ansible.errors import AnsibleError
@@ -28,17 +27,12 @@ class _DummyTemplar:
return value
class _FakeContainerLookup:
def run(self, terms, variables=None, **kwargs):
assert terms == ["web-app-matrix", "directories.instance"], terms
return ["/opt/instances/matrix/"]
def _vars(**extra):
base = {
"application_id": "web-app-matrix",
"MATRIX_BRIDGES": [{"bridge_name": "meta"}, {"bridge_name": "signal"}],
"MATRIX_REGISTRATION_FILE_FOLDER": "/data/",
"MATRIX_BRIDGE_SOURCE_DIR": "/opt/instances/matrix/config/mautrix",
"MATRIX_BRIDGE_CONFIG_TARGET": "/config/config.yaml",
"MATRIX_REGISTRATION_FILE_FOLDER": "/registrations/",
}
base.update(extra)
return base
@@ -49,50 +43,90 @@ class TestMatrixBridgeMounts(unittest.TestCase):
def setUpClass(cls):
cls.module = _load_module()
def _make(self, variables):
def _run(self, terms, vars_=None):
vars_ = _vars() if vars_ is None else vars_
lm = self.module.LookupModule()
lm._templar = _DummyTemplar(variables)
lm._templar = _DummyTemplar(vars_)
lm._loader = None
return lm
return lm.run(terms, variables=vars_)
def _run(self, vars_):
lm = self._make(vars_)
with mock.patch.object(self.module, "lookup_loader") as loader_mock:
loader_mock.get.return_value = _FakeContainerLookup()
return lm.run([], variables=vars_)
def test_one_mount_per_bridge(self):
result = self._run(_vars())
def test_stack_declares_both_files_of_every_bridge(self):
self.assertEqual(
result,
self._run(["stack"]),
[
{
"mautrix_meta_config": {
"file": "/opt/instances/matrix/config/mautrix/meta/config.yaml"
},
"mautrix_meta_registration": {
"file": "/opt/instances/matrix/config/mautrix/meta/registration.yaml"
},
"mautrix_signal_config": {
"file": "/opt/instances/matrix/config/mautrix/signal/config.yaml"
},
"mautrix_signal_registration": {
"file": "/opt/instances/matrix/config/mautrix/signal/registration.yaml"
},
}
],
)
def test_synapse_mounts_one_registration_per_bridge(self):
self.assertEqual(
self._run(["synapse"]),
[
[
"/opt/instances/matrix/mautrix/meta:/data/mautrix-meta:ro",
"/opt/instances/matrix/mautrix/signal:/data/mautrix-signal:ro",
{
"source": "mautrix_meta_registration",
"target": "/registrations/mautrix-meta/registration.yaml",
"mode": 0o444,
},
{
"source": "mautrix_signal_registration",
"target": "/registrations/mautrix-signal/registration.yaml",
"mode": 0o444,
},
]
],
)
def test_no_bridges_yields_empty_list(self):
result = self._run(_vars(MATRIX_BRIDGES=[]))
self.assertEqual(result, [[]])
def test_bridge_mounts_only_its_own_config(self):
self.assertEqual(
self._run(["bridge", "signal"]),
[
[
{
"source": "mautrix_signal_config",
"target": "/config/config.yaml",
"mode": 0o444,
}
]
],
)
def test_terms_raise(self):
lm = self._make(_vars())
def test_no_bridges_declares_nothing(self):
vars_ = _vars(MATRIX_BRIDGES=[])
self.assertEqual(self._run(["stack"], vars_), [{}])
self.assertEqual(self._run(["synapse"], vars_), [[]])
def test_unknown_view_raises(self):
for terms in ([], ["x"]):
with self.assertRaises(AnsibleError):
self._run(terms)
def test_bridge_that_is_not_enabled_raises(self):
with self.assertRaises(AnsibleError):
lm.run(["x"], variables=_vars())
self._run(["bridge", "whatsapp"])
def test_missing_bridges_raises(self):
vars_ = _vars()
del vars_["MATRIX_BRIDGES"]
lm = self._make(vars_)
with self.assertRaises(AnsibleError):
lm.run([], variables=vars_)
self._run(["stack"], vars_)
def test_non_list_bridges_raises(self):
lm = self._make(_vars(MATRIX_BRIDGES="meta"))
with self.assertRaises(AnsibleError):
lm.run([], variables=_vars(MATRIX_BRIDGES="meta"))
self._run(["stack"], _vars(MATRIX_BRIDGES="meta"))
if __name__ == "__main__":