mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 19:32:17 +00:00
feat(web-app-matrix): integrate five MDAD addons and repair three dead upstream flags
Add baibot, synapse-usage-exporter, hookshot-webhooks and hookshot-feeds as
declared addons, and make the existing hookshot and jitsi addons reach the
default flavor for the first time.
Three template lines named variables matrix-docker-ansible-deploy never reads,
so the switches they were meant to flip stayed at their upstream defaults:
* matrix_hookshot_{gitlab,jira}_enabled -> matrix_bridge_hookshot_*_enabled
* matrix_bridge_mautrix_meta_enabled -> the Messenger and Instagram switches,
which upstream keeps as two separate roles
* matrix_bridge_mautrix_gitter_enabled, which has no upstream role at all, is
removed together with its dead services.matrix.plugins.gitter key
A bridge addon now names the switches it drives under config.upstream_flags,
next to its bridge_name, so the mapping lives with the package instead of in a
hand-written template line per bridge. test_flavor_package_superset checks that
declaration rather than grepping the template, which a loop would defeat.
services.matrix.plugins.hookshot follows its partners' deployment instead of a
static false, so the bridge comes up wherever web-app-gitlab or web-app-n8n is
deployed; n8n joins meta/services.yml as a consumer with the variant coverage
the dynamic-flag contract requires.
Synapse metrics now reach the outer Prometheus. The inner listener binds to the
matrix-mdad container's own interface rather than loopback, because Prometheus
joins this role's docker network and scrapes the container by name, and
native_metrics resolves service_key and port per flavor instead of claiming the
compose flavor is the only one with a target.
jitsi_enabled follows services.jitsi.shared. Enabled implies shared in the base
config, so MDAD's own conferencing stack never starts and Element is pointed at
the co-deployed web-app-jitsi through the client config and the .well-known
property; previously MDAD raised web, prosody, jicofo and jvb in every ansible
round on a domain the outer proxy never routed.
Variant 3 pairs the ansible flavor with svc-ai-litellm and the bridge addons so
baibot and the five mautrix bridges are exercised in the default flavor at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2556ca4631
commit
b3e0dce578
28 files changed
+676
-26
No files matched your search
@@ -96,11 +96,65 @@ bridge's `config.yaml` and the matching Synapse `registration.yaml` from the sam
|
||||
| `mautrix-signal` | `bridge` | disabled | external network |
|
||||
| `mautrix-slack` | `bridge` | disabled | external network |
|
||||
| `mautrix-meta` | `bridge` | disabled | external network |
|
||||
| `hookshot-gitlab` | `bridge` | follows `services.gitlab.enabled` | `gitlab` |
|
||||
| `hookshot-jira` | `bridge` | never: `services.jira.enabled` is pinned off because `web-app-jira` is end of life (see [lifecycle.md](../../docs/contributing/design/role/services/lifecycle.md)) | `jira` |
|
||||
| `hookshot-webhooks` | `bridge` | follows `services.n8n.enabled` | `n8n` |
|
||||
| `hookshot-feeds` | `bridge` | disabled | RSS, no partner role |
|
||||
| `jitsi` | `module` | follows `services.jitsi.enabled` | `jitsi` |
|
||||
| `baibot` | `addon` | follows `services.matrix.plugins.chatgpt` | `litellm` |
|
||||
| `synapse-usage-exporter` | `addon` | follows `services.prometheus.enabled` | `prometheus` |
|
||||
|
||||
The compose flavor derives `MATRIX_BRIDGES` from the enabled bridge addons'
|
||||
`config:` blocks; the enabled/disabled split is exercised by the compose
|
||||
variant in [`meta/variants.yml`](./meta/variants.yml). Coverage is via
|
||||
`test-bridge-roster.js`.
|
||||
`config:` blocks that declare a `bridge_name`; the enabled/disabled split is
|
||||
exercised by the compose variant in [`meta/variants.yml`](./meta/variants.yml).
|
||||
Coverage is via `test-bridge-roster.js`.
|
||||
|
||||
The three cross-role addons follow their partner's service flag instead of a
|
||||
static default, except where the partner is end of life: `services.jira.enabled`
|
||||
is a literal `false` that no inventory can turn on, so `hookshot-jira` stays
|
||||
declared and dormant. `hookshot-gitlab` and `hookshot-jira` select which
|
||||
[matrix-hookshot](https://github.com/matrix-org/matrix-hookshot) connections the
|
||||
ansible flavor configures, on top of the `services.matrix.plugins.hookshot`
|
||||
knob that decides whether MDAD deploys the bridge at all. `jitsi` writes
|
||||
[Element's](https://github.com/element-hq/element-web/blob/develop/docs/jitsi.md)
|
||||
`jitsi.preferredDomain` so conference widgets open on the co-deployed
|
||||
`web-app-jitsi` instance: the compose flavor writes it into
|
||||
`element.config.json`, the ansible flavor overrides MDAD's
|
||||
`matrix_client_element_jitsi_preferred_domain` and the matching `.well-known`
|
||||
client property. Which of the two stacks serves is decided by
|
||||
`services.jitsi.shared`: shared means `web-app-jitsi` provides it and MDAD's own
|
||||
jitsi role stays off, unshared means MDAD raises web, prosody, jicofo and jvb
|
||||
inside the runner container and owns the client config itself. The unshared
|
||||
branch serves on `matrix_server_fqn_jitsi`, which needs a routed domain before
|
||||
it is reachable from outside the runner. Per-addon specs live under
|
||||
`files/playwright/addons/`.
|
||||
|
||||
`services.matrix.plugins.hookshot` follows the deployment of the partners its
|
||||
connection addons bind, so the bridge comes up wherever `web-app-gitlab` or
|
||||
`web-app-n8n` is deployed. The four `hookshot-*` addons then select which
|
||||
connections MDAD configures through `matrix_bridge_hookshot_<kind>_enabled`;
|
||||
`generic` and `feeds` are upstream defaults the addons drive explicitly.
|
||||
`hookshot-feeds` has no partner service of its own, so enabling it alone does
|
||||
nothing: without `web-app-gitlab` or `web-app-n8n` the bridge it configures is
|
||||
never deployed. Pin it in a round that already carries one of them.
|
||||
|
||||
A bridge addon names the upstream switches it drives under
|
||||
`config.upstream_flags`, next to its `bridge_name`. The ansible flavor renders
|
||||
that list; one addon may drive several switches, as `mautrix-meta` does for
|
||||
MDAD's separate Messenger and Instagram roles.
|
||||
|
||||
`services.matrix.plugins.chatgpt` is the single switch for the AI surface in
|
||||
both flavors: the compose flavor renders `matrix-chatgpt-bot`, the ansible
|
||||
flavor enables `baibot`. Both reach the model through `svc-ai-litellm` with the
|
||||
role's `litellm_api_key`, and MDAD creates the `baibot` account itself from
|
||||
`matrix_user_creator_users_auto`, so no account provisioning step is needed.
|
||||
|
||||
`synapse-usage-exporter` turns Synapse's metrics listener on and publishes it on
|
||||
the `matrix-mdad` container's own interface. The outer Prometheus joins this
|
||||
role's docker network and scrapes the DiD container by name, which is why
|
||||
`services.prometheus.native_metrics` resolves its `service_key` and `port` per
|
||||
flavor: `synapse:9000` for the in-repo compose stack, `matrix:9100` for the
|
||||
MDAD stack where the homeserver runs one docker layer deeper.
|
||||
|
||||
## Conferencing
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const { test, expect } = require("../onion-test");
|
||||
const { resolveTimeout } = require("../timeouts");
|
||||
const { skipUnlessAddonEnabled } = require("../addon-gating");
|
||||
const { skipUnlessServiceEnabled } = require("../service-gating");
|
||||
const shared = require("../_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
const BAIBOT_LOCALPART = "baibot";
|
||||
|
||||
test("baibot addon: the bot user is provisioned on the Synapse homeserver", async ({ request }) => {
|
||||
skipUnlessAddonEnabled("baibot");
|
||||
skipUnlessServiceEnabled("litellm");
|
||||
test.skip(
|
||||
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("ansible"),
|
||||
"baibot ships with the matrix-docker-ansible-deploy flavor only",
|
||||
);
|
||||
|
||||
test.setTimeout(resolveTimeout(60_000));
|
||||
|
||||
const matrixBaseUrl = shared.env.matrixBaseUrl;
|
||||
const matrixServerName = shared.env.matrixServerName;
|
||||
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set").toBeTruthy();
|
||||
expect(matrixServerName, "MATRIX_SERVER_NAME must be set").toBeTruthy();
|
||||
|
||||
const botUserId = `@${BAIBOT_LOCALPART}:${matrixServerName}`;
|
||||
const profileUrl = `${matrixBaseUrl}/_matrix/client/v3/profile/${encodeURIComponent(botUserId)}`;
|
||||
|
||||
const response = await request.get(profileUrl, { failOnStatusCode: false, timeout: resolveTimeout(30_000) });
|
||||
|
||||
expect(
|
||||
response.status(),
|
||||
`the baibot user ${botUserId} must be registered on Synapse (${matrixBaseUrl}). MDAD creates it from ` +
|
||||
`matrix_user_creator_users_auto whenever the bot is enabled and carries a password, so a 404 means either ` +
|
||||
`the password reached the renderer empty or user creation never ran. Got HTTP ${response.status()}.`,
|
||||
).toBe(200);
|
||||
|
||||
const profile = await response.json().catch(() => null);
|
||||
expect(
|
||||
profile && typeof profile === "object" && !Array.isArray(profile),
|
||||
`Synapse must return the baibot profile JSON object for ${botUserId}, proving the bot user exists`,
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
const { test, expect } = require("../onion-test");
|
||||
const { resolveTimeout } = require("../timeouts");
|
||||
const { skipUnlessAddonEnabled } = require("../addon-gating");
|
||||
const { skipUnlessServiceEnabled } = require("../service-gating");
|
||||
const { decodeDotenvQuotedValue } = require("../personas");
|
||||
const shared = require("../_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
const HOOKSHOT_BOT_LOCALPART = "hookshot";
|
||||
|
||||
const FEED_PATH = "/explore/projects.atom";
|
||||
|
||||
async function loginWithPassword(request, matrixBaseUrl, username, password) {
|
||||
const response = await request.post(`${matrixBaseUrl}/_matrix/client/v3/login`, {
|
||||
failOnStatusCode: false,
|
||||
timeout: resolveTimeout(30_000),
|
||||
data: {
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: username },
|
||||
password,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
response.status(),
|
||||
`biber must obtain a client-server access token by password; the deploy provisions the account, ` +
|
||||
`so a 403 here means the password in the Playwright env no longer matches Synapse. ` +
|
||||
`HTTP ${response.status()}: ${(await response.text().catch(() => "")).slice(0, 300)}`,
|
||||
).toBe(200);
|
||||
return (await response.json()).access_token;
|
||||
}
|
||||
|
||||
async function botNotices(request, matrixBaseUrl, token, roomId, botUserId) {
|
||||
const response = await request.get(
|
||||
`${matrixBaseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages?dir=b&limit=50`,
|
||||
{ failOnStatusCode: false, timeout: resolveTimeout(30_000), headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
if (response.status() !== 200) return [];
|
||||
const body = await response.json().catch(() => ({}));
|
||||
return (body.chunk || [])
|
||||
.filter((event) => event && event.type === "m.room.message" && event.sender === botUserId)
|
||||
.map((event) => String((event.content && event.content.body) || ""));
|
||||
}
|
||||
|
||||
test("hookshot-feeds addon: biber subscribes a room to an RSS feed and hookshot confirms", async ({ request }) => {
|
||||
skipUnlessAddonEnabled("hookshot-feeds");
|
||||
skipUnlessServiceEnabled("gitlab");
|
||||
test.skip(
|
||||
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("ansible"),
|
||||
"hookshot ships with the matrix-docker-ansible-deploy flavor only",
|
||||
);
|
||||
|
||||
const plugins = JSON.parse(decodeDotenvQuotedValue(process.env.MATRIX_PLUGINS_JSON || "") || "{}");
|
||||
test.skip(
|
||||
!plugins.hookshot,
|
||||
`services.matrix.plugins.hookshot is off, so MDAD renders no hookshot bridge (MATRIX_PLUGINS_JSON: ${JSON.stringify(plugins)})`,
|
||||
);
|
||||
|
||||
test.setTimeout(resolveTimeout(240_000));
|
||||
|
||||
const matrixBaseUrl = (shared.env.matrixBaseUrl || "").replace(/\/$/, "");
|
||||
const matrixServerName = shared.env.matrixServerName;
|
||||
const gitlabBaseUrl = (decodeDotenvQuotedValue(process.env.GITLAB_BASE_URL) || "").replace(/\/$/, "");
|
||||
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
|
||||
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
|
||||
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set").toBeTruthy();
|
||||
expect(gitlabBaseUrl, "GITLAB_BASE_URL must be set while the gitlab service is enabled").toBeTruthy();
|
||||
expect(biberUsername, "BIBER_USERNAME must be set").toBeTruthy();
|
||||
|
||||
const botUserId = `@${HOOKSHOT_BOT_LOCALPART}:${matrixServerName}`;
|
||||
const feedUrl = `${gitlabBaseUrl}${FEED_PATH}`;
|
||||
const token = await loginWithPassword(request, matrixBaseUrl, biberUsername, biberPassword);
|
||||
const auth = { Authorization: `Bearer ${token}` };
|
||||
|
||||
const createResponse = await request.post(`${matrixBaseUrl}/_matrix/client/v3/createRoom`, {
|
||||
failOnStatusCode: false,
|
||||
timeout: resolveTimeout(60_000),
|
||||
headers: auth,
|
||||
data: { preset: "private_chat", name: "hookshot feed probe", invite: [botUserId] },
|
||||
});
|
||||
expect(
|
||||
createResponse.status(),
|
||||
`creating the probe room must succeed: ${(await createResponse.text().catch(() => "")).slice(0, 300)}`,
|
||||
).toBe(200);
|
||||
const roomId = (await createResponse.json()).room_id;
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const members = await request.get(
|
||||
`${matrixBaseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/joined_members`,
|
||||
{ failOnStatusCode: false, timeout: resolveTimeout(30_000), headers: auth },
|
||||
);
|
||||
if (members.status() !== 200) return false;
|
||||
return Object.keys((await members.json()).joined || {}).includes(botUserId);
|
||||
},
|
||||
{
|
||||
timeout: resolveTimeout(120_000),
|
||||
message: `${botUserId} must accept the invite; if it never joins, the hookshot container is not running or failed to authenticate against Synapse, so no feed command can be processed`,
|
||||
},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const sendResponse = await request.put(
|
||||
`${matrixBaseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(`feed-probe-${process.pid}`)}`,
|
||||
{
|
||||
failOnStatusCode: false,
|
||||
timeout: resolveTimeout(30_000),
|
||||
headers: auth,
|
||||
data: { msgtype: "m.text", body: `!hookshot feed ${feedUrl}` },
|
||||
},
|
||||
);
|
||||
expect(
|
||||
sendResponse.status(),
|
||||
`sending the feed command must succeed: ${(await sendResponse.text().catch(() => "")).slice(0, 300)}`,
|
||||
).toBe(200);
|
||||
|
||||
let replies = [];
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
replies = await botNotices(request, matrixBaseUrl, token, roomId, botUserId);
|
||||
return replies.length > 0;
|
||||
},
|
||||
{
|
||||
timeout: resolveTimeout(120_000),
|
||||
message: `${botUserId} must answer the feed command. Silence means the feeds module is off: MDAD renders it from matrix_bridge_hookshot_feeds_enabled, which follows the hookshot-feeds addon`,
|
||||
},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
expect(
|
||||
replies.some((body) => body.includes("Room configured to bridge")),
|
||||
`hookshot must confirm the subscription with "Room configured to bridge". It validates a feed by fetching ` +
|
||||
`it, so "Could not read feed from URL" means ${feedUrl} is not readable from inside the runner and the ` +
|
||||
`probe needs a different feed source, not that the addon is broken. Replies: ${JSON.stringify(replies)}`,
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
const { test, expect } = require("../onion-test");
|
||||
const { resolveTimeout } = require("../timeouts");
|
||||
const { skipUnlessAddonEnabled } = require("../addon-gating");
|
||||
const { skipUnlessServiceEnabled } = require("../service-gating");
|
||||
const { decodeDotenvQuotedValue } = require("../personas");
|
||||
const shared = require("../_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
const HOOKSHOT_BOT_LOCALPART = "hookshot";
|
||||
|
||||
test("hookshot-webhooks addon: the hookshot appservice bot is provisioned on the Synapse homeserver", async ({ request }) => {
|
||||
skipUnlessAddonEnabled("hookshot-webhooks");
|
||||
skipUnlessServiceEnabled("n8n");
|
||||
test.skip(
|
||||
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("ansible"),
|
||||
"hookshot ships with the matrix-docker-ansible-deploy flavor only",
|
||||
);
|
||||
|
||||
const plugins = JSON.parse(decodeDotenvQuotedValue(process.env.MATRIX_PLUGINS_JSON || "") || "{}");
|
||||
test.skip(
|
||||
!plugins.hookshot,
|
||||
`services.matrix.plugins.hookshot is off, so MDAD renders no hookshot bridge (MATRIX_PLUGINS_JSON: ${JSON.stringify(plugins)})`,
|
||||
);
|
||||
|
||||
test.setTimeout(resolveTimeout(60_000));
|
||||
|
||||
const matrixBaseUrl = shared.env.matrixBaseUrl;
|
||||
const matrixServerName = shared.env.matrixServerName;
|
||||
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set").toBeTruthy();
|
||||
expect(matrixServerName, "MATRIX_SERVER_NAME must be set").toBeTruthy();
|
||||
|
||||
const botUserId = `@${HOOKSHOT_BOT_LOCALPART}:${matrixServerName}`;
|
||||
const profileUrl = `${matrixBaseUrl.replace(/\/$/, "")}/_matrix/client/v3/profile/${encodeURIComponent(botUserId)}`;
|
||||
|
||||
const response = await request.get(profileUrl, { failOnStatusCode: false, timeout: resolveTimeout(30_000) });
|
||||
|
||||
expect(
|
||||
response.status(),
|
||||
`the hookshot appservice bot ${botUserId} must be registered on Synapse (${matrixBaseUrl}) so an inbound ` +
|
||||
`generic webhook from n8n can be routed into a room. A 404 means the appservice registration never landed. ` +
|
||||
`Got HTTP ${response.status()}.`,
|
||||
).toBe(200);
|
||||
});
|
||||
@@ -10,10 +10,6 @@ test.use({ ignoreHTTPSErrors: true });
|
||||
test("jitsi addon: Element advertises the co-deployed Jitsi deployment as its preferred domain", async ({ request }) => {
|
||||
skipUnlessAddonEnabled("jitsi");
|
||||
skipUnlessServiceEnabled("jitsi");
|
||||
test.skip(
|
||||
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("compose"),
|
||||
"the ansible flavor lets matrix-docker-ansible-deploy render Element's config.json",
|
||||
);
|
||||
|
||||
test.setTimeout(resolveTimeout(60_000));
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const { test, expect } = require("../onion-test");
|
||||
const { resolveTimeout } = require("../timeouts");
|
||||
const { skipUnlessAddonEnabled } = require("../addon-gating");
|
||||
const { skipUnlessServiceEnabled } = require("../service-gating");
|
||||
const shared = require("../_shared");
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
test("synapse-usage-exporter addon: the usage metrics never reach the public vhost", async ({ request }) => {
|
||||
skipUnlessAddonEnabled("synapse-usage-exporter");
|
||||
skipUnlessServiceEnabled("prometheus");
|
||||
|
||||
test.setTimeout(resolveTimeout(60_000));
|
||||
|
||||
const matrixBaseUrl = shared.env.matrixBaseUrl;
|
||||
expect(matrixBaseUrl, "MATRIX_BASE_URL must be set").toBeTruthy();
|
||||
|
||||
const metricsUrl = `${matrixBaseUrl.replace(/\/$/, "")}/_synapse/metrics`;
|
||||
const response = await request.get(metricsUrl, { failOnStatusCode: false, timeout: resolveTimeout(30_000) });
|
||||
const status = response.status();
|
||||
const body = await response.text();
|
||||
|
||||
expect(
|
||||
status,
|
||||
`enabling the usage exporter turns Synapse's own metrics listener on, and that listener carries room counts and ` +
|
||||
`user activity. It MUST stay inside the stack for the Prometheus scrape, never on the public vhost at ` +
|
||||
`${metricsUrl}. HTTP 200 here means the reverse proxy forwards it to the world. Got HTTP ${status}.`,
|
||||
).not.toBe(200);
|
||||
|
||||
expect(
|
||||
body.includes("synapse_build_info") || body.includes("# TYPE synapse"),
|
||||
`the public response for ${metricsUrl} must not contain Prometheus exposition text; got: ${body.slice(0, 200)}`,
|
||||
).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
enabled: "{{ lookup('config', 'web-app-matrix', 'services.matrix.plugins.chatgpt') | bool }}"
|
||||
required: false
|
||||
mechanism: addon
|
||||
source: upstream
|
||||
bridges:
|
||||
- litellm
|
||||
config:
|
||||
user_localpart: baibot
|
||||
agent_id: litellm
|
||||
user_password: "{{ lookup('config', 'web-app-matrix', 'secrets.credentials.baibot_user_password', '') }}"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
enabled: false
|
||||
required: false
|
||||
mechanism: bridge
|
||||
source: upstream
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
enabled: "{{ lookup('config', 'web-app-matrix', 'services.n8n.enabled') | bool }}"
|
||||
required: false
|
||||
mechanism: bridge
|
||||
source: upstream
|
||||
bridges:
|
||||
- n8n
|
||||
@@ -9,6 +9,9 @@ update:
|
||||
upstream_id: mautrix-meta
|
||||
config:
|
||||
bridge_name: "meta"
|
||||
upstream_flags:
|
||||
- matrix_bridge_mautrix_meta_messenger_enabled
|
||||
- matrix_bridge_mautrix_meta_instagram_enabled
|
||||
appservice_port: 29319
|
||||
database_username: "mautrix_meta_bridge"
|
||||
database_name: "mautrix_meta_bridge"
|
||||
|
||||
@@ -9,6 +9,8 @@ update:
|
||||
upstream_id: mautrix-signal
|
||||
config:
|
||||
bridge_name: "signal"
|
||||
upstream_flags:
|
||||
- matrix_bridge_mautrix_signal_enabled
|
||||
appservice_port: 29328
|
||||
database_username: "mautrix_signal_bridge"
|
||||
database_name: "mautrix_signal_bridge"
|
||||
|
||||
@@ -9,6 +9,8 @@ update:
|
||||
upstream_id: mautrix-slack
|
||||
config:
|
||||
bridge_name: "slack"
|
||||
upstream_flags:
|
||||
- matrix_bridge_mautrix_slack_enabled
|
||||
appservice_port: 29335
|
||||
database_username: "mautrix_slack_bridge"
|
||||
database_name: "mautrix_slack_bridge"
|
||||
|
||||
@@ -9,6 +9,8 @@ update:
|
||||
upstream_id: mautrix-telegram
|
||||
config:
|
||||
bridge_name: "telegram"
|
||||
upstream_flags:
|
||||
- matrix_bridge_mautrix_telegram_enabled
|
||||
appservice_port: 29317
|
||||
database_username: "mautrix_telegram_bridge"
|
||||
database_name: "mautrix_telegram_bridge"
|
||||
|
||||
@@ -9,6 +9,8 @@ update:
|
||||
upstream_id: mautrix-whatsapp
|
||||
config:
|
||||
bridge_name: "whatsapp"
|
||||
upstream_flags:
|
||||
- matrix_bridge_mautrix_whatsapp_enabled
|
||||
appservice_port: 29318
|
||||
database_username: "mautrix_whatsapp_bridge"
|
||||
database_name: "mautrix_whatsapp_bridge"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
enabled: "{{ lookup('config', 'web-app-matrix', 'services.prometheus.enabled') | bool }}"
|
||||
required: false
|
||||
mechanism: addon
|
||||
source: upstream
|
||||
bridges:
|
||||
- prometheus
|
||||
@@ -42,6 +42,10 @@ credentials:
|
||||
description: Matrix user password used by the ChatGPT bridge
|
||||
algorithm: sha256
|
||||
validation: ^[a-f0-9]{64}$
|
||||
baibot_user_password:
|
||||
description: Matrix user password baibot logs in with, also used by MDAD to create the bot account
|
||||
algorithm: sha256
|
||||
validation: ^[a-f0-9]{64}$
|
||||
mautrix_meta_bridge_database_password:
|
||||
description: Database password for the mautrix-meta bridge (Facebook Messenger + Instagram)
|
||||
algorithm: bcrypt
|
||||
|
||||
@@ -30,6 +30,21 @@ email:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-mailu' in group_names }}"
|
||||
shared: "{{ 'web-app-mailu' in group_names }}"
|
||||
gitlab:
|
||||
bond: 0.5
|
||||
enabled: "{{ 'web-app-gitlab' in group_names }}"
|
||||
shared: "{{ 'web-app-gitlab' in group_names }}"
|
||||
jira:
|
||||
enabled: false # nocheck: dynamic-flag - web-app-jira is EOL, no host may bind it
|
||||
shared: false # nocheck: dynamic-flag - web-app-jira is EOL, no host may bind it
|
||||
n8n:
|
||||
bond: 0.5
|
||||
enabled: "{{ 'web-app-n8n' in group_names }}"
|
||||
shared: "{{ 'web-app-n8n' in group_names }}"
|
||||
jitsi:
|
||||
bond: 0.5
|
||||
enabled: "{{ 'web-app-jitsi' in group_names }}"
|
||||
shared: "{{ 'web-app-jitsi' in group_names }}"
|
||||
# nocheck: playwright-service-flag — TURN/STUN server, no Playwright surface (provider: web-svc-coturn)
|
||||
coturn:
|
||||
bond: 1
|
||||
@@ -103,9 +118,9 @@ prometheus:
|
||||
enabled: "{{ 'web-app-prometheus' in group_names }}"
|
||||
shared: "{{ 'web-app-prometheus' in group_names }}"
|
||||
native_metrics:
|
||||
enabled: true
|
||||
port: 9000
|
||||
service_key: synapse
|
||||
enabled: true # nocheck: dynamic-flag — both flavors publish a scrape target, so no inventory decides this
|
||||
port: "{{ 9000 if lookup('config', 'web-app-matrix', 'services.matrix.flavor') == 'compose' else 9100 }}"
|
||||
service_key: "{{ 'synapse' if lookup('config', 'web-app-matrix', 'services.matrix.flavor') == 'compose' else 'matrix' }}"
|
||||
communication:
|
||||
channel: true
|
||||
# nocheck: playwright-service-flag: shared provider entity; the app itself is covered by this role's own Playwright spec
|
||||
@@ -145,9 +160,8 @@ matrix:
|
||||
appservice_irc: false
|
||||
chatgpt: "{{ 'svc-ai-litellm' in group_names }}"
|
||||
discord: false
|
||||
gitter: false
|
||||
heisenbridge: true
|
||||
hookshot: false
|
||||
hookshot: "{{ 'web-app-gitlab' in group_names or 'web-app-n8n' in group_names }}"
|
||||
mautrix_discord: false
|
||||
mautrix_signal: false
|
||||
mautrix_slack: false
|
||||
@@ -156,7 +170,6 @@ matrix:
|
||||
mautrix_whatsapp: false
|
||||
conferencing:
|
||||
element_call: true
|
||||
jitsi: true
|
||||
run_after:
|
||||
- web-app-matomo
|
||||
- web-app-keycloak
|
||||
@@ -169,7 +182,6 @@ matrix:
|
||||
pids_limit: 256
|
||||
|
||||
# nocheck: playwright-service-flag
|
||||
# nocheck: variants-coverage — sso-variant-tor requires the onion on in every variant, so the false polarity has no variant left to live in
|
||||
tor:
|
||||
bond: 0.5
|
||||
enabled: "{{ 'svc-net-tor' in group_names }}"
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
---
|
||||
# nocheck: variant-limit — variant 3 is the only round that pairs the ansible
|
||||
# flavor with svc-ai-litellm. Without it the baibot addon is enabled solely in
|
||||
# variant 2, whose compose flavor never renders the MDAD vars that deploy it, so
|
||||
# the default flavor's AI surface would ship untested.
|
||||
#
|
||||
# nocheck: sso-variant-tor — variant 1 is the round that turns gitlab, n8n and
|
||||
# jitsi on. jitsi is clearnet-only (its meta/services.yml pins tor off), and
|
||||
# tor.exclusive swaps this role's domains for the onion, so an onion-only
|
||||
# homeserver cannot pair with it. The hookshot bridges and Element's jitsi
|
||||
# conferencing are therefore driven from a clearnet homeserver origin; variant 0
|
||||
# pairs the onion with SSO.
|
||||
|
||||
- services:
|
||||
container_backup:
|
||||
enabled: true
|
||||
matrix:
|
||||
flavor: ansible
|
||||
n8n:
|
||||
enabled: false
|
||||
shared: false
|
||||
coturn:
|
||||
enabled: true
|
||||
shared: true
|
||||
@@ -16,6 +31,12 @@
|
||||
email:
|
||||
enabled: true
|
||||
shared: true
|
||||
gitlab:
|
||||
enabled: false
|
||||
shared: false
|
||||
jitsi:
|
||||
enabled: false
|
||||
shared: false
|
||||
ldap:
|
||||
enabled: true
|
||||
shared: true
|
||||
@@ -59,6 +80,15 @@
|
||||
email:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
gitlab:
|
||||
enabled: true
|
||||
shared: true
|
||||
n8n:
|
||||
enabled: true
|
||||
shared: true
|
||||
jitsi:
|
||||
enabled: true
|
||||
shared: true
|
||||
ldap:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
@@ -81,7 +111,10 @@
|
||||
seaweedfs:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
tor:
|
||||
tor: # nocheck: variant-tor-collateral jitsi refuses the onion and this is the only round pairing it with the homeserver, so gitlab and n8n ride along on clearnet; a tor round of this role has no budget left for either partner
|
||||
enabled: false
|
||||
addons:
|
||||
hookshot-feeds:
|
||||
enabled: true
|
||||
|
||||
- services:
|
||||
@@ -90,6 +123,9 @@
|
||||
shared: false # nocheck: variants-static-override backup shared stays literally true
|
||||
matrix:
|
||||
flavor: compose
|
||||
n8n:
|
||||
enabled: false
|
||||
shared: false
|
||||
seaweedfs:
|
||||
enabled: true
|
||||
shared: true
|
||||
@@ -105,6 +141,12 @@
|
||||
email:
|
||||
enabled: true
|
||||
shared: true
|
||||
gitlab:
|
||||
enabled: false
|
||||
shared: false
|
||||
jitsi:
|
||||
enabled: false
|
||||
shared: false
|
||||
ldap:
|
||||
enabled: true
|
||||
shared: true
|
||||
@@ -137,3 +179,66 @@
|
||||
enabled: true
|
||||
mautrix-meta:
|
||||
enabled: true
|
||||
|
||||
- services:
|
||||
container_backup:
|
||||
enabled: false
|
||||
shared: false # nocheck: variants-static-override base shared is a static literal and this round joins no backup host
|
||||
matrix:
|
||||
flavor: ansible
|
||||
coturn:
|
||||
enabled: false
|
||||
shared: false
|
||||
css:
|
||||
enabled: false
|
||||
shared: false
|
||||
dashboard:
|
||||
enabled: false
|
||||
shared: false
|
||||
email:
|
||||
enabled: false
|
||||
shared: false
|
||||
gitlab:
|
||||
enabled: false
|
||||
shared: false
|
||||
n8n:
|
||||
enabled: false
|
||||
shared: false
|
||||
jitsi:
|
||||
enabled: false
|
||||
shared: false
|
||||
ldap:
|
||||
enabled: false
|
||||
shared: false
|
||||
logout:
|
||||
enabled: false
|
||||
shared: false
|
||||
matomo:
|
||||
enabled: false
|
||||
shared: false
|
||||
postgres:
|
||||
shared: true
|
||||
litellm:
|
||||
enabled: true
|
||||
sso:
|
||||
enabled: false
|
||||
shared: false
|
||||
prometheus:
|
||||
enabled: false
|
||||
shared: false
|
||||
seaweedfs:
|
||||
enabled: false
|
||||
shared: false
|
||||
tor:
|
||||
enabled: true
|
||||
addons:
|
||||
mautrix-whatsapp:
|
||||
enabled: true
|
||||
mautrix-telegram:
|
||||
enabled: true
|
||||
mautrix-signal:
|
||||
enabled: true
|
||||
mautrix-slack:
|
||||
enabled: true
|
||||
mautrix-meta:
|
||||
enabled: true
|
||||
@@ -0,0 +1 @@
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
---
|
||||
@@ -7,6 +7,8 @@
|
||||
| selectattr('value.mechanism', 'equalto', 'bridge')
|
||||
| selectattr('value.enabled', 'equalto', true)
|
||||
| map(attribute='value.config')
|
||||
| select('mapping')
|
||||
| selectattr('bridge_name', 'defined')
|
||||
| list }}
|
||||
changed_when: false
|
||||
|
||||
|
||||
@@ -146,12 +146,41 @@ matrix_bridge_heisenbridge_enabled: {{ MATRIX_BRIDGE.heisenbridge | string | low
|
||||
matrix_bridge_hookshot_enabled: {{ MATRIX_BRIDGE.hookshot | string | lower }}
|
||||
matrix_bridge_appservice_discord_enabled: {{ MATRIX_BRIDGE.discord | string | lower }}
|
||||
matrix_bridge_mautrix_discord_enabled: {{ MATRIX_BRIDGE.mautrix_discord | string | lower }}
|
||||
matrix_bridge_mautrix_slack_enabled: {{ MATRIX_BRIDGE.mautrix_slack | string | lower }}
|
||||
matrix_bridge_mautrix_signal_enabled: {{ MATRIX_BRIDGE.mautrix_signal | string | lower }}
|
||||
matrix_bridge_mautrix_telegram_enabled: {{ MATRIX_BRIDGE.mautrix_telegram | string | lower }}
|
||||
matrix_bridge_mautrix_twitter_enabled: {{ MATRIX_BRIDGE.mautrix_twitter | string | lower }}
|
||||
matrix_bridge_mautrix_whatsapp_enabled: {{ MATRIX_BRIDGE.mautrix_whatsapp | string | lower }}
|
||||
matrix_bridge_mautrix_gitter_enabled: {{ MATRIX_BRIDGE.gitter | string | lower }}
|
||||
{% for bridge_addon in MATRIX_BRIDGE_ADDONS %}
|
||||
{% for upstream_flag in bridge_addon.value.config.upstream_flags %}
|
||||
{{ upstream_flag }}: {{ bridge_addon.value.enabled | string | lower }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if MATRIX_BRIDGE.hookshot %}
|
||||
matrix_bridge_hookshot_gitlab_enabled: {{ MATRIX_ADDON_HOOKSHOT_GITLAB_ENABLED | string | lower }}
|
||||
matrix_bridge_hookshot_jira_enabled: {{ MATRIX_ADDON_HOOKSHOT_JIRA_ENABLED | string | lower }}
|
||||
matrix_bridge_hookshot_generic_enabled: {{ MATRIX_ADDON_HOOKSHOT_WEBHOOKS_ENABLED | string | lower }}
|
||||
matrix_bridge_hookshot_feeds_enabled: {{ MATRIX_ADDON_HOOKSHOT_FEEDS_ENABLED | string | lower }}
|
||||
{% endif %}
|
||||
|
||||
jitsi_enabled: {{ MATRIX_CONFERENCING_JITSI | string | lower }}
|
||||
matrix_bot_baibot_enabled: {{ MATRIX_ADDON_BAIBOT_ENABLED | string | lower }}
|
||||
{% if MATRIX_ADDON_BAIBOT_ENABLED %}
|
||||
matrix_bot_baibot_config_user_mxid_localpart: "{{ MATRIX_ADDON_BAIBOT_LOCALPART }}"
|
||||
matrix_bot_baibot_config_user_password: "{{ MATRIX_ADDON_BAIBOT_PASSWORD }}"
|
||||
matrix_bot_baibot_config_agents_static_definitions_openai_enabled: true
|
||||
matrix_bot_baibot_config_agents_static_definitions_openai_id: "{{ MATRIX_ADDON_BAIBOT_AGENT_ID }}"
|
||||
matrix_bot_baibot_config_agents_static_definitions_openai_config_base_url: "{{ LITELLM_OPENAI_BASE_LOCAL_URL }}"
|
||||
matrix_bot_baibot_config_agents_static_definitions_openai_config_api_key: "{{ MATRIX_CHATGPT_BRIDGE_API_KEY }}"
|
||||
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_model_id: "{{ LITELLM_CHAT_MODEL }}"
|
||||
matrix_bot_baibot_config_initial_global_config_handler_catch_all: "static/{{ MATRIX_ADDON_BAIBOT_AGENT_ID }}"
|
||||
{% endif %}
|
||||
|
||||
matrix_synapse_usage_exporter_enabled: {{ MATRIX_ADDON_SYNAPSE_USAGE_EXPORTER_ENABLED | string | lower }}
|
||||
{% if MATRIX_ADDON_SYNAPSE_USAGE_EXPORTER_ENABLED %}
|
||||
matrix_synapse_metrics_enabled: true
|
||||
matrix_synapse_container_metrics_api_host_bind_port: "0.0.0.0:{{ MATRIX_SYNAPSE_METRICS_PORT }}"
|
||||
matrix_synapse_metrics_port: {{ MATRIX_SYNAPSE_METRICS_PORT }}
|
||||
{% endif %}
|
||||
|
||||
jitsi_enabled: {{ MATRIX_JITSI_SELF_HOSTED | string | lower }}
|
||||
{% if MATRIX_ADDON_JITSI_ENABLED and not MATRIX_JITSI_SELF_HOSTED | bool %}
|
||||
matrix_client_element_jitsi_preferred_domain: "{{ MATRIX_ADDON_JITSI_PREFERRED_DOMAIN }}"
|
||||
matrix_static_files_file_matrix_client_property_io_element_jitsi_preferred_domain: "{{ MATRIX_ADDON_JITSI_PREFERRED_DOMAIN }}"
|
||||
{% endif %}
|
||||
matrix_rtc_enabled: {{ MATRIX_CONFERENCING_ELEMENT_CALL | string | lower }}
|
||||
@@ -52,6 +52,11 @@
|
||||
"feature_rust_crypto": true,
|
||||
"feature_new_room_decoration_ui": true
|
||||
},
|
||||
{% if MATRIX_ADDON_JITSI_ENABLED | bool %}
|
||||
"jitsi": {
|
||||
"preferredDomain": "{{ MATRIX_ADDON_JITSI_PREFERRED_DOMAIN }}"
|
||||
},
|
||||
{% endif %}
|
||||
"element_call": {
|
||||
"url": "https://call.element.dev"
|
||||
},
|
||||
|
||||
@@ -19,6 +19,8 @@ ELEMENT_CALL_SERVICE_ENABLED={{ MATRIX_CONFERENCING_ELEMENT_CALL | string | lowe
|
||||
# nocheck: playwright-service-gate — covered by sys-svc-mail integration tests, not the persona surface
|
||||
EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }}
|
||||
|
||||
GITLAB_BASE_URL={{ lookup('tls', 'web-app-gitlab', 'url.base') | dotenv_quote }}
|
||||
|
||||
GITLAB_SERVICE_ENABLED={{ lookup('config', application_id, 'services.gitlab.enabled') | string | lower }}
|
||||
|
||||
JIRA_SERVICE_ENABLED={{ lookup('config', application_id, 'services.jira.enabled') | string | lower }}
|
||||
@@ -62,6 +64,10 @@ SEAWEEDFS_APP_SECRET_KEY={{ lookup('objstore', application_id, 'secret_key') | d
|
||||
|
||||
SEAWEEDFS_REGION={{ lookup('config', 'web-svc-seaweedfs', 'services.seaweedfs.region') }}
|
||||
|
||||
N8N_SERVICE_ENABLED={{ lookup('config', application_id, 'services.n8n.enabled') | string | lower }}
|
||||
|
||||
PROMETHEUS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.prometheus.enabled') | string | lower }}
|
||||
|
||||
SEAWEEDFS_S3_URL={{ lookup('canonical_url', 'web-svc-seaweedfs', 'api', consumer=application_id) | dotenv_quote }}
|
||||
|
||||
SEAWEEDFS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.seaweedfs.enabled') | string | lower }}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Matrix Synapse — server and federation metrics.
|
||||
# Requires enable_metrics: true in homeserver.yaml.
|
||||
# Enable via native_metrics.enabled: true in inventory.
|
||||
- job_name: "matrix-synapse"
|
||||
scheme: http
|
||||
metrics_path: /_synapse/metrics
|
||||
|
||||
@@ -78,11 +78,28 @@ MATRIX_MDAD_DAEMON_JSON:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
MATRIX_BRIDGE: "{{ lookup('config', application_id, 'services.matrix.plugins') }}"
|
||||
MATRIX_BRIDGE_ADDONS: "{{ lookup('config', application_id, 'addons') | dict2items | selectattr('value.mechanism', 'equalto', 'bridge') | selectattr('value.config.bridge_name',
|
||||
'defined') | list }}"
|
||||
MATRIX_BRIDGE: "{{ lookup('config', application_id, 'services.matrix.plugins') | combine(dict(MATRIX_BRIDGE_ADDONS | map(attribute='value.config.bridge_name') | map('regex_replace',
|
||||
'^', 'mautrix_') | zip(MATRIX_BRIDGE_ADDONS | map(attribute='value.enabled')))) }}"
|
||||
|
||||
MATRIX_CONFERENCING_JITSI: "{{ lookup('config', application_id, 'services.matrix.conferencing.jitsi') | bool }}"
|
||||
MATRIX_CONFERENCING_ELEMENT_CALL: "{{ lookup('config', application_id, 'services.matrix.conferencing.element_call') | bool }}"
|
||||
|
||||
MATRIX_ADDON_BAIBOT_ENABLED: "{{ lookup('config', application_id, 'addons.baibot.enabled') | bool }}"
|
||||
MATRIX_ADDON_BAIBOT_LOCALPART: "{{ lookup('config', application_id, 'addons.baibot.config.user_localpart') }}"
|
||||
MATRIX_ADDON_BAIBOT_PASSWORD: "{{ lookup('config', application_id, 'addons.baibot.config.user_password') }}"
|
||||
MATRIX_ADDON_BAIBOT_AGENT_ID: "{{ lookup('config', application_id, 'addons.baibot.config.agent_id') }}"
|
||||
MATRIX_ADDON_SYNAPSE_USAGE_EXPORTER_ENABLED: "{{ lookup('config', application_id, 'addons.synapse-usage-exporter.enabled') | bool }}"
|
||||
MATRIX_SYNAPSE_METRICS_PORT: "{{ lookup('config', application_id, 'services.prometheus.native_metrics.port') }}"
|
||||
|
||||
MATRIX_ADDON_HOOKSHOT_GITLAB_ENABLED: "{{ lookup('config', application_id, 'addons.hookshot-gitlab.enabled') | bool }}"
|
||||
MATRIX_ADDON_HOOKSHOT_JIRA_ENABLED: "{{ lookup('config', application_id, 'addons.hookshot-jira.enabled') | bool }}"
|
||||
MATRIX_ADDON_HOOKSHOT_WEBHOOKS_ENABLED: "{{ lookup('config', application_id, 'addons.hookshot-webhooks.enabled') | bool }}"
|
||||
MATRIX_ADDON_HOOKSHOT_FEEDS_ENABLED: "{{ lookup('config', application_id, 'addons.hookshot-feeds.enabled') | bool }}"
|
||||
MATRIX_JITSI_SELF_HOSTED: "{{ (lookup('config', application_id, 'services.jitsi.enabled') | bool) and not (lookup('config', application_id, 'services.jitsi.shared') | bool) }}"
|
||||
MATRIX_ADDON_JITSI_ENABLED: "{{ lookup('config', application_id, 'addons.jitsi.enabled') | bool }}"
|
||||
MATRIX_ADDON_JITSI_PREFERRED_DOMAIN: "{{ lookup('config', application_id, 'addons.jitsi.config.preferred_domain') }}"
|
||||
|
||||
MATRIX_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') | bool }}"
|
||||
|
||||
MATRIX_CA_INJECTED: "{{ lookup('ca_injected', application_id) | bool }}"
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""A role's default flavor MUST address every package its other flavors can.
|
||||
|
||||
A role that installs itself in more than one flavor keeps one set of packages
|
||||
per flavor, and the two sets are derived from different places. For
|
||||
``web-app-matrix`` the compose flavor builds its bridge loop from
|
||||
``meta/addons/`` (``tasks/flavor/compose/main.yml`` selects the addons whose
|
||||
``mechanism`` is ``bridge`` and whose ``config.bridge_name`` is set), while the
|
||||
default ansible flavor addresses packages through a mapping constant in
|
||||
``vars/main.yml`` that pairs each package key with the upstream switches it
|
||||
drives.
|
||||
|
||||
Nothing ties those two sources together, so a package added as an addon is
|
||||
invisible to the default flavor until someone also adds its key to that
|
||||
mapping, and a deployment that switches flavor silently loses it. The default
|
||||
flavor is therefore required to be the superset: every bridge the non-default
|
||||
flavor can construct must have a key the default flavor can address. The
|
||||
reverse is allowed - the default flavor may know packages no addon declares.
|
||||
|
||||
The check compares the two derived sets rather than grepping the templates,
|
||||
because the compose flavor iterates its bridges instead of naming them and a
|
||||
token scan would read that as "supports none". The default flavor iterates too,
|
||||
so its side is read from the mapping constant rather than from the rendered
|
||||
variable names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from utils.cache.files import read_text
|
||||
from utils.cache.yaml import load_yaml_str
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
ROLES_DIR = PROJECT_ROOT / "roles"
|
||||
BRIDGE_MECHANISM = "bridge"
|
||||
PLUGIN_PREFIX = "mautrix_"
|
||||
|
||||
|
||||
def _bridge_names(role_dir) -> set[str]:
|
||||
"""Every ``config.bridge_name`` a role's bridge addons declare.
|
||||
|
||||
Args:
|
||||
role_dir: the role's directory.
|
||||
"""
|
||||
names: set[str] = set()
|
||||
for path in sorted((role_dir / "meta" / "addons").glob("*.yml")):
|
||||
spec = load_yaml_str(read_text(str(path))) or {}
|
||||
if spec.get("mechanism") != BRIDGE_MECHANISM:
|
||||
continue
|
||||
name = (spec.get("config") or {}).get("bridge_name")
|
||||
if isinstance(name, str) and name:
|
||||
names.add(name)
|
||||
return names
|
||||
|
||||
|
||||
def _unaddressed_bridges(role_dir) -> list[str]:
|
||||
"""Bridge addons that name no upstream switch for the default flavor.
|
||||
|
||||
Args:
|
||||
role_dir: the role's directory.
|
||||
"""
|
||||
offenders: list[str] = []
|
||||
for path in sorted((role_dir / "meta" / "addons").glob("*.yml")):
|
||||
spec = load_yaml_str(read_text(str(path))) or {}
|
||||
if spec.get("mechanism") != BRIDGE_MECHANISM:
|
||||
continue
|
||||
config = spec.get("config") or {}
|
||||
name = config.get("bridge_name")
|
||||
if not isinstance(name, str) or not name:
|
||||
continue
|
||||
flags = config.get("upstream_flags")
|
||||
if not isinstance(flags, list) or not [f for f in flags if str(f).strip()]:
|
||||
offenders.append(path.stem)
|
||||
return offenders
|
||||
|
||||
|
||||
def _multi_flavor_roles() -> list:
|
||||
"""Role directories that ship more than one ``templates/flavor/`` tree."""
|
||||
return [
|
||||
flavor_dir.parent.parent
|
||||
for flavor_dir in sorted(ROLES_DIR.glob("*/templates/flavor"))
|
||||
if len([d for d in flavor_dir.iterdir() if d.is_dir()]) > 1
|
||||
]
|
||||
|
||||
|
||||
class TestFlavorPackageSuperset(unittest.TestCase):
|
||||
def test_the_default_flavor_addresses_every_bridge_addon(self) -> None:
|
||||
offenders: list[str] = []
|
||||
for role_dir in _multi_flavor_roles():
|
||||
offenders.extend(
|
||||
f"{role_dir.name}: addons.{addon_id} builds a bridge in the "
|
||||
"non-default flavor but declares no config.upstream_flags, so the "
|
||||
"default flavor renders no switch for it"
|
||||
for addon_id in _unaddressed_bridges(role_dir)
|
||||
)
|
||||
|
||||
if offenders:
|
||||
self.fail(
|
||||
f"{len(offenders)} bridge addon(s) the default flavor cannot "
|
||||
"address. List the upstream switches under config.upstream_flags "
|
||||
"so both flavors deploy the same set:\n" + "\n".join(offenders)
|
||||
)
|
||||
|
||||
def test_the_check_sees_at_least_one_multi_flavor_role(self) -> None:
|
||||
roles = _multi_flavor_roles()
|
||||
self.assertTrue(
|
||||
roles,
|
||||
"no role ships two flavor trees any more, so this guard passes "
|
||||
"vacuously and needs rewriting rather than silently succeeding",
|
||||
)
|
||||
self.assertTrue(
|
||||
any(_bridge_names(role_dir) for role_dir in roles),
|
||||
"no multi-flavor role declares a bridge addon with a bridge_name, so "
|
||||
"the superset comparison has nothing to compare",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user