mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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 of93735fd533. 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:
1 parent
a650801b35
commit
304144ce67
20 files changed
+280
-196
No files matched your search
@@ -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', '') }}"
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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') }}"
|
||||
|
||||
+67
-33
@@ -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__":
|
||||
|
||||
Reference in new issue
Block a user