dev #239
12
.env.example
12
.env.example
@@ -51,11 +51,13 @@ SEAFILE_ADMIN_PASSWORD=change-this-seafile-admin-password
|
||||
|
||||
M2M_API_KEY=change-this-m2m-key
|
||||
API_BASE_URL=https://app.example.com/backend
|
||||
# Collaboration: public browser URL and canonical HTTPS API address (including reverse-proxy prefix).
|
||||
FEDEO_PUBLIC_URL=https://app.example.com
|
||||
FEDEO_FEDERATION_URL=https://app.example.com/backend
|
||||
# Requires mutual tenant partner approval under Zusammenarbeit.
|
||||
FEDEO_FEDERATION_ENABLED=false
|
||||
# Collaboration uses API_BASE_URL for federation and its origin for guest links.
|
||||
# Optional overrides only, e.g. when using a separate federation gateway.
|
||||
FEDEO_PUBLIC_URL=
|
||||
FEDEO_FEDERATION_URL=
|
||||
# auto: enabled with a valid HTTPS API address and ENCRYPTION_KEY; false: opt out.
|
||||
# Partners still require mutual tenant approval under Zusammenarbeit.
|
||||
FEDEO_FEDERATION_ENABLED=auto
|
||||
GOCARDLESS_BASE_URL=https://api.gocardless.com
|
||||
GOCARDLESS_SECRET_ID=replace-this
|
||||
GOCARDLESS_SECRET_KEY=replace-this
|
||||
|
||||
@@ -87,6 +87,8 @@ Minimal wichtige Werte fur den ersten Start:
|
||||
- `DATABASE_URL=postgres://fedeo:<starkes-passwort>@db:5432/fedeo`
|
||||
- `API_BASE_URL=https://app.example.com/backend`
|
||||
|
||||
Die Chat-Föderation verwendet ebenfalls `API_BASE_URL`; Gastlinks übernehmen deren Ursprung. Mit gültiger öffentlicher HTTPS-Adresse auf Port 443 und vorhandenem gültigem `ENCRYPTION_KEY` ist sie automatisch verfügbar. Zusätzliche URLs sind nur bei abweichender Infrastruktur nötig (`FEDEO_FEDERATION_URL`, `FEDEO_PUBLIC_URL`). `FEDEO_FEDERATION_ENABLED=false` schaltet sie ausdrücklich ab; der Standard ist `auto`. Der Updater übernimmt die passenden Compose-Einträge aus dem Release. Partner müssen anschließend weiterhin je Mandant freigegeben werden; siehe [Föderation einrichten](docs/chat-foederation-konzept.md#15-implementierter-stand-und-pilotbetrieb).
|
||||
|
||||
Wenn du MinIO verwendest, setze zusatzlich:
|
||||
|
||||
- `S3_ENDPOINT=http://minio:9000`
|
||||
|
||||
30
backend/src/modules/collaboration/config.ts
Normal file
30
backend/src/modules/collaboration/config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { instanceUrl } from "./transport"
|
||||
|
||||
type Settings = {
|
||||
API_BASE_URL?: string;
|
||||
ENCRYPTION_KEY?: string;
|
||||
FEDEO_PUBLIC_URL?: string;
|
||||
FEDEO_FEDERATION_URL?: string;
|
||||
FEDEO_FEDERATION_ENABLED?: string;
|
||||
}
|
||||
|
||||
/** Existing deployment settings are sufficient; extra URLs are optional overrides. */
|
||||
export function collaborationConfig(settings: Settings) {
|
||||
const configuredApi = settings.API_BASE_URL?.trim() || ""
|
||||
let apiUrl = (settings.FEDEO_FEDERATION_URL?.trim() || configuredApi).replace(/\/+$/, "")
|
||||
let publicUrl = settings.FEDEO_PUBLIC_URL?.trim().replace(/\/+$/, "") || ""
|
||||
if (!publicUrl) {
|
||||
try { publicUrl = new URL(configuredApi || apiUrl).origin } catch { /* Local development may have no public URL. */ }
|
||||
}
|
||||
const mode = settings.FEDEO_FEDERATION_ENABLED?.trim().toLowerCase() || "auto"
|
||||
if (!["auto", "true", "false"].includes(mode)) throw new Error("FEDEO_FEDERATION_ENABLED muss auto, true oder false sein")
|
||||
if (mode === "false") return { apiUrl, publicUrl, enabled: false, disabledReason: "Die Föderation wurde vom Betreiber deaktiviert." }
|
||||
|
||||
let disabledReason: string | undefined
|
||||
try { apiUrl = instanceUrl(apiUrl) }
|
||||
catch { disabledReason = "Föderation benötigt eine gültige öffentliche HTTPS-API_BASE_URL auf Port 443." }
|
||||
if (!disabledReason && !/^[a-f0-9]{64}$/i.test(settings.ENCRYPTION_KEY || "")) {
|
||||
disabledReason = "Föderation benötigt einen gültigen ENCRYPTION_KEY mit 64 Hex-Zeichen." }
|
||||
if (mode === "true" && disabledReason) throw new Error(disabledReason)
|
||||
return { apiUrl, publicUrl, enabled: !disabledReason, disabledReason }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { FederationKey, Headers, instanceUrl, safeTransport, signedHeaders, Tran
|
||||
|
||||
type Options = {
|
||||
apiUrl: string; enabled: boolean;
|
||||
disabledReason?: string;
|
||||
seal: (value: string) => any; unseal: (value: any) => string;
|
||||
transport?: Transport;
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export class FederationService {
|
||||
this.transport = options.transport || safeTransport
|
||||
}
|
||||
enabled() {
|
||||
if (!this.options.enabled) fail("Der Betreiber muss die Föderation zuerst aktivieren", 503)
|
||||
if (!this.options.enabled) fail(this.options.disabledReason || "Der Betreiber muss die Föderation zuerst aktivieren", 503)
|
||||
instanceUrl(this.origin)
|
||||
}
|
||||
async keys() {
|
||||
@@ -71,7 +72,7 @@ export class FederationService {
|
||||
VALUES (${ctx.tenantId},${origin},${instanceId},${keyId},${publicKey}) ON CONFLICT(tenant_id,origin) DO UPDATE SET active=true,updated_at=now()`)
|
||||
}
|
||||
}
|
||||
return { enabled: this.options.enabled, origin: this.origin, partners: (await rows(this.service.db,
|
||||
return { enabled: this.options.enabled, disabledReason: this.options.disabledReason, origin: this.origin, partners: (await rows(this.service.db,
|
||||
sql`SELECT origin,active,public_key FROM collaboration_partners WHERE tenant_id=${ctx.tenantId} ORDER BY origin`)).map(p => ({ origin: p.origin, active: p.active, fingerprint: hash(p.public_key) })) }
|
||||
}
|
||||
async call(tenantId: number, origin: string, action: string, payload: any) {
|
||||
|
||||
@@ -4,16 +4,18 @@ import { sendMail } from "../../utils/mailer"
|
||||
import { CollaborationService } from "./service"
|
||||
import { FederationService } from "./federation"
|
||||
import { encrypt, decrypt } from "../../utils/crypt"
|
||||
import { collaborationConfig } from "./config"
|
||||
|
||||
export function collaborationRuntime(server: FastifyInstance) {
|
||||
const apiUrl = String(process.env.FEDEO_FEDERATION_URL || secrets.API_BASE_URL || "").replace(/\/$/, "")
|
||||
const publicUrl = String(process.env.FEDEO_PUBLIC_URL || (apiUrl ? new URL(apiUrl).origin : "")).replace(/\/$/, "")
|
||||
const { apiUrl, publicUrl, enabled, disabledReason } = collaborationConfig({
|
||||
...process.env, API_BASE_URL: secrets.API_BASE_URL, ENCRYPTION_KEY: secrets.ENCRYPTION_KEY,
|
||||
})
|
||||
const service = new CollaborationService(server.db, {
|
||||
publicUrl,
|
||||
guestPepper: () => secrets.ENCRYPTION_KEY || "",
|
||||
sendMail,
|
||||
})
|
||||
const federation = new FederationService(service, { apiUrl, enabled: process.env.FEDEO_FEDERATION_ENABLED === "true", seal: encrypt, unseal: decrypt })
|
||||
const federation = new FederationService(service, { apiUrl, enabled, disabledReason, seal: encrypt, unseal: decrypt })
|
||||
service.options.changed = (roomId, db) => federation.changed(roomId, db)
|
||||
service.options.invited = (ctx, roomId, target, invitationId, db) => federation.invite(ctx, roomId, target, invitationId, db)
|
||||
let timer: ReturnType<typeof setInterval>
|
||||
|
||||
54
backend/tests/collaborationConfig.test.ts
Normal file
54
backend/tests/collaborationConfig.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { collaborationConfig } from "../src/modules/collaboration/config"
|
||||
|
||||
const deployment = { API_BASE_URL: "https://fedeo.example/backend", ENCRYPTION_KEY: "a".repeat(64) }
|
||||
|
||||
test("existing HTTPS deployment enables federation without additional environment values", () => {
|
||||
const config = collaborationConfig(deployment)
|
||||
assert.equal(config.enabled, true)
|
||||
assert.equal(config.apiUrl, deployment.API_BASE_URL)
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(config.disabledReason, undefined)
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "auto", FEDEO_FEDERATION_URL: "", FEDEO_PUBLIC_URL: "" }).enabled, true)
|
||||
})
|
||||
|
||||
test("custom API paths are retained and instance addresses are canonicalized", () => {
|
||||
const config = collaborationConfig({ ...deployment, API_BASE_URL: "https://FEDEO.example:443/application/api/" })
|
||||
assert.equal(config.apiUrl, "https://fedeo.example/application/api")
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(config.enabled, true)
|
||||
})
|
||||
|
||||
test("explicit opt-out stays disabled even on a correctly configured deployment", () => {
|
||||
const config = collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "false" })
|
||||
assert.equal(config.enabled, false)
|
||||
assert.match(config.disabledReason!, /Betreiber deaktiviert/)
|
||||
})
|
||||
|
||||
test("optional gateway override does not move guest links away from the web application", () => {
|
||||
const config = collaborationConfig({ ...deployment, FEDEO_FEDERATION_URL: "https://gateway.example/federation-api/" })
|
||||
assert.equal(config.apiUrl, "https://gateway.example/federation-api")
|
||||
assert.equal(config.publicUrl, "https://fedeo.example")
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_PUBLIC_URL: "https://portal.example/app/" }).publicUrl, "https://portal.example/app")
|
||||
})
|
||||
|
||||
test("local and incomplete deployments keep local collaboration available without federation", () => {
|
||||
for (const API_BASE_URL of [undefined, "", "invalid", "http://localhost:3100", "https://fedeo.example:8443/backend", "https://user:password@fedeo.example/backend"]) {
|
||||
const config = collaborationConfig({ ...deployment, API_BASE_URL })
|
||||
assert.equal(config.enabled, false, API_BASE_URL)
|
||||
assert.match(config.disabledReason!, /HTTPS-API_BASE_URL/)
|
||||
}
|
||||
for (const ENCRYPTION_KEY of [undefined, "", "invalid", "x".repeat(64)]) {
|
||||
const config = collaborationConfig({ ...deployment, ENCRYPTION_KEY })
|
||||
assert.equal(config.enabled, false)
|
||||
assert.match(config.disabledReason!, /ENCRYPTION_KEY/)
|
||||
}
|
||||
})
|
||||
|
||||
test("forced activation reports invalid configuration instead of silently ignoring it", () => {
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true", API_BASE_URL: "http://localhost" }), /HTTPS/)
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true", ENCRYPTION_KEY: "invalid" }), /ENCRYPTION_KEY/)
|
||||
assert.throws(() => collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "typo" }), /auto, true oder false/)
|
||||
assert.equal(collaborationConfig({ ...deployment, FEDEO_FEDERATION_ENABLED: "true" }).enabled, true)
|
||||
})
|
||||
@@ -125,7 +125,7 @@ services:
|
||||
API_BASE_URL: ${API_BASE_URL}
|
||||
FEDEO_PUBLIC_URL: ${FEDEO_PUBLIC_URL:-}
|
||||
FEDEO_FEDERATION_URL: ${FEDEO_FEDERATION_URL:-}
|
||||
FEDEO_FEDERATION_ENABLED: ${FEDEO_FEDERATION_ENABLED:-false}
|
||||
FEDEO_FEDERATION_ENABLED: ${FEDEO_FEDERATION_ENABLED:-auto}
|
||||
GOCARDLESS_BASE_URL: ${GOCARDLESS_BASE_URL}
|
||||
GOCARDLESS_SECRET_ID: ${GOCARDLESS_SECRET_ID}
|
||||
GOCARDLESS_SECRET_KEY: ${GOCARDLESS_SECRET_KEY}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Konzept: Föderierte Chats und externe Projektteilnehmer
|
||||
|
||||
Stand: 30. September 2026. Das ursprüngliche Zielbild basiert auf Commit `df48c976`. Die Schritte 0, 1a, 1b und ein föderierter Web-Pilot aus Schritt 2 sind inzwischen implementiert (`21fe4723`, `ea641bb4`, `7b5696ed`). Die folgenden Architekturkapitel enthalten weiterhin spätere Ausbaustufen; den tatsächlichen Lieferumfang, die Einrichtung und verbleibende Grenzen beschreibt Abschnitt 15. Es wurden keine produktiven Instanzen verändert oder echten Einladungen versendet.
|
||||
Stand: 2. Oktober 2026. Das ursprüngliche Zielbild basiert auf Commit `df48c976`. Die Schritte 0, 1a, 1b und ein föderierter Web-Pilot aus Schritt 2 sind inzwischen implementiert (`21fe4723`, `ea641bb4`, `7b5696ed`). Die folgenden Architekturkapitel enthalten weiterhin spätere Ausbaustufen; den tatsächlichen Lieferumfang, die Einrichtung und verbleibende Grenzen beschreibt Abschnitt 15. Es wurden keine produktiven Instanzen verändert oder echten Einladungen versendet.
|
||||
|
||||
## 1. Empfehlung
|
||||
|
||||
@@ -298,12 +298,14 @@ Bei einem vorübergehend nicht erreichbaren Eigentümer sind bereits geladene Na
|
||||
### Einrichtung
|
||||
|
||||
1. Vor dem Backendstart die additiven Migrationen `0075_project_collaboration.sql` und `0076_collaboration_federation.sql` über den bestehenden Migrationslauf einspielen. Der Selfhost-Container führt standardmäßig `npm run migrate` aus. Bestehende interne Chats bleiben getrennt; kein vorhandener Chat wird automatisch freigegeben.
|
||||
2. `FEDEO_PUBLIC_URL` auf die öffentliche Webadresse setzen. Für Gastcodes und optional versendete Einladungs-E-Mails muss der bestehende SMTP-Versand funktionieren. `ENCRYPTION_KEY` muss wie für die übrige Backend-Verschlüsselung ein stabiler 64-stelliger Hex-Schlüssel sein.
|
||||
3. Für den instanzübergreifenden Pilotbetrieb `FEDEO_FEDERATION_URL` auf die kanonische öffentliche HTTPS-API-Adresse setzen, einschließlich des Reverse-Proxy-Präfixes, etwa `https://fedeo.example/backend`. Anschließend `FEDEO_FEDERATION_ENABLED=true` setzen. Die Variablen sind in `.env.example` und `docker-compose.selfhost.yml` aufgenommen. Föderation ist standardmäßig deaktiviert; lokale Unterräume und Gäste funktionieren unabhängig davon.
|
||||
2. Die vorhandene `API_BASE_URL` wird als Föderationsadresse verwendet, einschließlich des Reverse-Proxy-Präfixes, etwa `https://fedeo.example/backend`. Gastlinks werden aus deren Ursprung `https://fedeo.example` abgeleitet. Zusätzliche URL-Variablen sind im Standardbetrieb nicht nötig. Für Gastcodes und optional versendete Einladungs-E-Mails muss der bestehende SMTP-Versand funktionieren. Den vorhandenen `ENCRYPTION_KEY` beibehalten; er muss ein stabiler 64-stelliger Hex-Schlüssel sein.
|
||||
3. Der Standard ist jetzt `FEDEO_FEDERATION_ENABLED=auto`, auch wenn die Variable nicht gesetzt ist. Mit gültiger HTTPS-API-Adresse auf Port 443 und gültigem Verschlüsselungsschlüssel wird Föderation automatisch bereitgestellt. Lokale HTTP-Entwicklungsumgebungen oder unvollständige Konfigurationen bleiben ohne Föderation startfähig; die Partnerverwaltung nennt den fehlenden Konfigurationswert. `false` schaltet Föderation ausdrücklich ab. `true` erzwingt die Aktivierung und meldet eine ungültige Konfiguration bereits beim Start. Nur bei abweichender Infrastruktur `FEDEO_FEDERATION_URL` oder `FEDEO_PUBLIC_URL` setzen; beide überschreiben die automatisch abgeleiteten Werte. Ein separates Föderationsgateway verändert die Gast-Webadresse nicht automatisch.
|
||||
4. Der Proxy muss `GET <API-Adresse>/.well-known/fedeo-federation` und `POST <API-Adresse>/federation/v1/rpc` durchreichen und darf JSON-Body oder Signaturheader nicht verändern. Externe Ziele benötigen öffentlich auflösbare HTTPS-Adressen auf Port 443. Private/reservierte IPv4- und IPv6-Ziele sowie Weiterleitungen werden abgewiesen; die geprüfte DNS-Adresse wird für die TLS-Verbindung festgehalten.
|
||||
5. In beiden beteiligten Mandanten die jeweils andere API-Adresse unter **Partnerinstanzen** freigeben. Discovery ermittelt Instanz-ID und öffentlichen Schlüssel über HTTPS. Die erste Freigabe pinnt diese Identität. Ein später abweichender Schlüssel wird abgelehnt und braucht eine gesonderte Betreiberprüfung; eine automatische Schlüsselrotation ist noch nicht vorhanden.
|
||||
6. Persönliche Kontaktadresse des Empfängers verwenden, gezielt in einen Unterraum einladen und beim Empfänger annehmen. Für einen ersten Pilotversuch anschließend Rechteänderung, Widerruf und eine vorübergehend unterbrochene Verbindung prüfen.
|
||||
|
||||
Der normale Selfhost-Updater übernimmt die Compose-Datei und Migrationen aus dem Zielrelease; ein manuelles Nachtragen der Föderationsvariablen ist bei verwalteten Installationen nicht erforderlich. Die vorhandene `.env` wird bis auf die Release-Version beibehalten. Eine darin oder in der Override-Datei gesetzte Abschaltung bleibt wirksam. Wurde `FEDEO_FEDERATION_ENABLED=false` aus der früheren Beispielkonfiguration übernommen, muss dieser Wert einmal entfernt oder auf `auto` gesetzt werden, wenn Föderation gewünscht ist. Die automatische Bereitstellung gibt keine Partner oder vorhandenen Chats frei; dafür bleiben die gegenseitige Mandantenfreigabe und explizite Einladung erforderlich.
|
||||
|
||||
Das feste Signaturprofil verwendet Ed25519 mit HTTP Message Signatures und Content-Digest. Signiert werden Methode, vollständige Zieladresse, Inhaltstyp, Inhaltsdigest und Herkunft; Zeitfenster und gespeicherte Nonces verhindern Replay. Das RPC-Protokoll ist Version 1 und ausschließlich für diese FEDEO-Implementierung bestimmt. Es ersetzt weder Matrix-Kompatibilität noch Ende-zu-Ende-Verschlüsselung.
|
||||
|
||||
### Speicherung, Betrieb und Wiederaufnahme
|
||||
@@ -329,3 +331,5 @@ COLLABORATION_TEST_DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:PORT/collab
|
||||
Jeder Datenbanktest erzeugt ein eigenes Schema und räumt es anschließend auf. Ohne gesetzte Testadresse werden die Datenbanktests bewusst übersprungen; der Datenbankname wird vor dem Anlegen geprüft. Backend-Typprüfung und Nuxt-Produktionsbuild waren erfolgreich, die abschließenden Vue-Komponenten lassen sich kompilieren. Eine visuelle Browserabnahme war in der Arbeitsumgebung mangels verbundenem Browser nicht möglich.
|
||||
|
||||
Die nächste Ausbaustufe aus Schritt 3 umfasst insbesondere native Mobile-Parität, Push-Benachrichtigungen, Moderation, feinere Kontingente sowie Schlüsselrotation und abgestimmte Wiederherstellung. Dateifreigaben, Aufgabenfreigaben und E2EE sind weiterhin Erweiterungen. Der hier implementierte Stand ist ein nutzbarer Web-Pilot für Chats und ausdrücklich begrenzte Projektzusammenarbeit; ein produktiver Rollout ist damit noch nicht erfolgt.
|
||||
|
||||
Die automatische Einrichtung wurde zusätzlich mit sechs Konfigurationstests und 58 Updater-Tests geprüft. Die Update-Regressionen verwenden echte Compose-Auswertung mit simuliertem Containerbetrieb und prüfen sowohl Installationen ohne Föderationsvariablen als auch den Erhalt einer ausdrücklichen Abschaltung und eigener URL-Vorgaben.
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
Der Updater verweigert externe Datenbanken, externen S3-/Seafile-Speicher, zusätzliche Dienste, Compose-Profile, geänderte Datenpfade und PostgreSQL-Imagewechsel. Dafür braucht es zunächst passende Backup-/Migrationsadapter. Eigene Einstellungen innerhalb der unterstützten Dienste können in `docker-compose.override.yml` **oder** `docker-compose.override.yaml` bleiben; der Updater verwendet diese Datei bei jedem Compose-Aufruf. Änderungen an dieser Override-Datei werden nicht überschrieben.
|
||||
|
||||
Für die Chat-Föderation übernimmt der Updater die neuen Backend-Einträge aus der Release-Compose-Datei automatisch. Zusätzliche Einträge in der bestehenden `.env` sind nicht nötig: Die Föderationsadresse folgt `API_BASE_URL`, Gastlinks verwenden deren Ursprung. Der Standard `FEDEO_FEDERATION_ENABLED=auto` aktiviert die Funktion bei gültiger HTTPS-Adresse auf Port 443 und gültigem `ENCRYPTION_KEY`. Ein vorhandenes `false` und eigene URL-Vorgaben bleiben erhalten. Partnerfreigaben und Einladungen werden weiterhin ausdrücklich je Mandant erteilt. Details stehen in der [Föderationsanleitung](chat-foederation-konzept.md#15-implementierter-stand-und-pilotbetrieb).
|
||||
|
||||
Eine gezielte Ausnahme ist die Stilllegung des **entfernten alten Matrix-Stacks**: `matrix-db`, `matrix-synapse`, `matrix-redis`, `matrix-element`, `matrix-livekit`, `matrix-rtc-jwt`, `matrix-turn` und `matrix-well-known` dürfen in der bestehenden Konfiguration vorkommen. Die Zielkonfiguration muss ausschließlich die FEDEO-Dienste des jeweiligen Speicherproviders enthalten, auch nach Zusammenführung mit einer Override-Datei. Matrix wird nicht in den neuen Stack übernommen. Bereits stillgelegte Matrix-Container bleiben erhalten und ihre lokalen Bind-Verzeichnisse werden weiter gesichert. Andere Zusatzdienste bleiben gesperrt; die Fehlermeldung nennt die konkreten Abweichungen. Ein Abbruch mit `mutationStarted: false` und `recoveryRequired: false` benötigt keine Wiederherstellung und keinen `resolve`-Aufruf.
|
||||
|
||||
Für diesen Übergang müssen die Matrix-Daten lokal unter `matrix/` liegen, insbesondere `matrix/postgres` und `matrix/synapse`; symbolische Links, andere konfigurierte Datenpfade, benannte Volumes und spezielle Compose-Konfigurationen benötigen eine manuelle Stilllegung. Die tatsächlichen Bind-Mounts der alten Container müssen zur Compose-Datei passen. Der Plan nennt `retiredServices` und `retainedDataPaths` und berücksichtigt den zusätzlichen Backup-Platz. Alle Matrix-Container werden vor dem Backup gestoppt. Das komplette lokale `matrix/`-Verzeichnis wird bei gestoppter Matrix-Datenbank in `files.tar` gesichert. Erst nach erfolgreichem Backup setzt der Updater die Neustartrichtlinie der alten Matrix-Container auf `no`, damit sie auch nach einem Host-Neustart stillgelegt bleiben. Er löscht weder diese Container noch ihre Daten oder Volumes. Image-seitig angelegte anonyme Volumes und schreibbare Container-Schichten bleiben an den alten Containern erhalten; sie sind nicht Bestandteil von `files.tar`. Die alten Container daher für die Wiederherstellung behalten und nicht mit `--remove-orphans` oder einem Docker-Prune entfernen.
|
||||
|
||||
@@ -92,7 +92,7 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
<section v-if="federation" class="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
|
||||
<div class="flex justify-between"><h2 class="font-semibold">Freigegebene Partnerinstanzen</h2><button @click="federation = null">Schließen</button></div>
|
||||
<p class="my-2 text-sm text-gray-500">Beide Firmen geben die jeweils andere Instanz frei. Einzelne Personen erhalten anschließend eine eigene Einladung.</p>
|
||||
<p v-if="!federation.enabled" class="my-2 text-sm text-amber-700">Die Föderation muss zuerst vom Betreiber aktiviert werden.</p>
|
||||
<p v-if="!federation.enabled" class="my-2 text-sm text-amber-700">{{ federation.disabledReason || 'Die Föderation muss zuerst vom Betreiber aktiviert werden.' }}</p>
|
||||
<form v-if="federation.enabled" class="flex flex-wrap gap-2" @submit.prevent="savePartner(partnerUrl)"><label class="flex-1"><span class="sr-only">Adresse der Partnerinstanz</span><input v-model="partnerUrl" type="url" required placeholder="https://partner.example/backend" class="field" /></label><UButton type="submit" :loading="busy">Partner freigeben</UButton></form>
|
||||
<div v-for="partner in federation.partners" :key="partner.origin" class="mt-3 flex items-center justify-between gap-3 text-sm"><span>{{ partner.origin }} · {{ partner.active ? 'Freigegeben' : 'Gesperrt' }}</span><UButton variant="outline" size="xs" :disabled="busy" @click="savePartner(partner.origin, !partner.active)">{{ partner.active ? 'Sperren' : 'Freigeben' }}</UButton></div>
|
||||
</section>
|
||||
|
||||
@@ -420,6 +420,40 @@ class UpdateTests(unittest.TestCase):
|
||||
self.assertLess(next(i for i, a in enumerate(calls) if 'pull' in a), next(i for i, a in enumerate(calls) if 'stop' in a))
|
||||
self.assertLess(next(i for i, a in enumerate(calls) if 'pg_restore' in a), next(i for i, a in enumerate(calls) if 'migrate' in a))
|
||||
|
||||
def use_pre_federation_installation(self, extra_env=b''):
|
||||
keys = (b'FEDEO_PUBLIC_URL', b'FEDEO_FEDERATION_URL', b'FEDEO_FEDERATION_ENABLED')
|
||||
self.original_env = b''.join(line for line in self.original_env.splitlines(True)
|
||||
if not line.startswith(keys)) + extra_env
|
||||
self.original_compose = b''.join(line for line in self.original_compose.splitlines(True)
|
||||
if not line.lstrip().startswith(keys))
|
||||
(self.root / '.env').write_bytes(self.original_env)
|
||||
(self.root / 'docker-compose.yml').write_bytes(self.original_compose)
|
||||
|
||||
def test_update_adds_federation_compose_defaults_without_requiring_new_env_settings(self):
|
||||
self.use_pre_federation_installation()
|
||||
result = self.run_update()
|
||||
self.assertEqual(result['status'], 'succeeded')
|
||||
env = self.updater.candidate['services']['backend']['environment']
|
||||
self.assertEqual(env['FEDEO_FEDERATION_ENABLED'], 'auto')
|
||||
self.assertEqual(env['API_BASE_URL'], 'https://app.example.com/backend')
|
||||
self.assertEqual(env['FEDEO_FEDERATION_URL'], '')
|
||||
self.assertEqual(env['FEDEO_PUBLIC_URL'], '')
|
||||
self.assertEqual((self.root / '.env').read_bytes(), u.set_version(self.original_env, '3.1.0'))
|
||||
self.assertIn(b'FEDEO_FEDERATION_ENABLED', (self.root / 'docker-compose.yml').read_bytes())
|
||||
|
||||
def test_update_preserves_explicit_federation_opt_out_and_url_overrides(self):
|
||||
self.use_pre_federation_installation(
|
||||
b'\nFEDEO_FEDERATION_ENABLED=false\n'
|
||||
b'FEDEO_FEDERATION_URL=https://gateway.example/backend\n'
|
||||
b'FEDEO_PUBLIC_URL=https://portal.example\n')
|
||||
result = self.run_update()
|
||||
self.assertEqual(result['status'], 'succeeded')
|
||||
env = self.updater.candidate['services']['backend']['environment']
|
||||
self.assertEqual(env['FEDEO_FEDERATION_ENABLED'], 'false')
|
||||
self.assertEqual(env['FEDEO_FEDERATION_URL'], 'https://gateway.example/backend')
|
||||
self.assertEqual(env['FEDEO_PUBLIC_URL'], 'https://portal.example')
|
||||
self.assertEqual((self.root / '.env').read_bytes(), u.set_version(self.original_env, '3.1.0'))
|
||||
|
||||
def test_check_only_creates_a_plan_and_ignores_unpublished_releases(self):
|
||||
result = self.updater.execute('check')
|
||||
self.assertEqual(result['version'], '3.1.0')
|
||||
|
||||
Reference in New Issue
Block a user