Update README section 14 with complete integration framework docs
Covers architecture, state management, full lifecycle (6 steps), both handlers with detailed occ commands and config patching, ReapplyConfigForTarget, force-recreate rationale, Traefik middleware for OO HTTPS proxy, UI on deploy page, wiring in main.go, and corrected API JSON field name (enabled not enable). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+47
-16
@@ -25,6 +25,7 @@ A single, lightweight Go container that replaces Portainer + scattered systemd s
|
||||
- [Asset Sync](#11-asset-sync)
|
||||
- [Debug Mode](#12-debug-mode)
|
||||
- [Geo-Restriction](#13-geo-restriction)
|
||||
- [App-to-App Integrations](#14-app-to-app-integrations)
|
||||
- [Repository Layout](#repository-layout)
|
||||
- [Configuration](#configuration)
|
||||
- [REST API](#rest-api)
|
||||
@@ -1199,13 +1200,19 @@ All mutating endpoints trigger an async Cloudflare sync. The `/api/geo/` path ac
|
||||
|
||||
### 14. App-to-App Integrations
|
||||
|
||||
Generic framework for connecting deployed applications to each other. Provider apps declare available integrations in `.felhom.yml`, and users enable/disable them via toggle switches on the provider's app info page.
|
||||
Generic framework for connecting deployed applications to each other. Provider apps declare available integrations in `.felhom.yml`, and users enable/disable them via toggle switches on the provider's deploy/settings page ("Beállítások").
|
||||
|
||||
#### Architecture (`internal/integrations/`)
|
||||
|
||||
- **`integrations.go`** — Core types: `Handler` interface (`Apply`/`Revoke`), `ApplyContext` (carries domain, decrypted env vars, restart func), `StatusInfo` (UI data)
|
||||
- **`manager.go`** — `Manager` coordinates toggle operations, builds apply contexts, lists integrations for UI. Uses `StackProvider` interface to break circular imports (adapted in main.go)
|
||||
- **`lifecycle.go`** — `OnStackStop` (suspend active integrations), `OnStackStart` (re-apply enabled integrations), `OnStackRemove` (permanent cleanup)
|
||||
- **`integrations.go`** — Core types: `Handler` interface (`Apply`/`Revoke`), `ApplyContext` (carries domain, decrypted env vars, provider metadata, stacks dir, logger, restart func), `StatusInfo` (UI data), `IntegrationKey()`/`ParseIntegrationKey()` key helpers
|
||||
- **`manager.go`** — `Manager` coordinates toggle operations, builds apply contexts from decrypted app.yaml env vars. Uses `StackProvider` interface (GetStack, GetStacks, RestartStack) to break circular imports with stacks package — adapted via `integrationStackAdapter` in main.go. Key methods:
|
||||
- `Toggle(ctx, provider, target, enable)` — Validates both apps deployed+running, calls Apply/Revoke, persists state
|
||||
- `ListForProvider(slug)` — Returns `[]StatusInfo` for UI with target deployment/running status
|
||||
- `ReapplyConfigForTarget(name)` — Re-applies all active integrations targeting a stack (config-only, no restart). Used by `SyncFileBrowserMounts` after config regeneration
|
||||
- **`lifecycle.go`** — Lifecycle hooks called from API router goroutines:
|
||||
- `OnStackStop` — Revokes active integrations, sets `"provider_stopped"`/`"target_unavailable"` (keeps `enabled=true`)
|
||||
- `OnStackStart` — Re-applies enabled integrations after 5s delay (waits for stack state refresh). Accepts both `StateRunning` and `StateStarting` via `isStackUp()` helper
|
||||
- `OnStackRemove` — Revokes and permanently deletes integration state
|
||||
- **Handler implementations** — One file per integration pair (e.g. `onlyoffice_filebrowser.go`, `onlyoffice_nextcloud.go`)
|
||||
|
||||
#### Integration State
|
||||
@@ -1216,26 +1223,41 @@ Stored in `settings.json` under `integrations` map (key: `"provider:target"`):
|
||||
- `last_error` — Most recent error message
|
||||
- `enabled_at` — RFC3339 timestamp
|
||||
|
||||
CRUD methods in settings.go: `GetIntegrationState`, `SetIntegrationState`, `RemoveIntegrationState`, `GetIntegrationsForProvider`, `GetIntegrationsForTarget` (all use existing RWMutex + atomic write pattern).
|
||||
|
||||
#### Lifecycle
|
||||
|
||||
1. **Enable**: User toggles on → validates both apps deployed+running → calls `Handler.Apply()` → persists state as `"active"`
|
||||
2. **Provider/target stops**: `OnStackStop` → calls `Handler.Revoke()` → sets status to `"provider_stopped"` or `"target_unavailable"` (keeps `enabled=true`)
|
||||
3. **Provider/target starts**: `OnStackStart` → finds enabled integrations with non-active status → re-applies if both sides running
|
||||
4. **Provider/target removed**: `OnStackRemove` → revokes and deletes integration state permanently
|
||||
5. **FileBrowser config regen**: `SyncFileBrowserMounts` overwrites `config.yaml` → `OnStackStart("filebrowser")` re-applies active integrations
|
||||
2. **Disable**: User toggles off → calls `Handler.Revoke()` → persists state as `"disabled"`
|
||||
3. **Provider/target stops**: `OnStackStop` → calls `Handler.Revoke()` → sets status to `"provider_stopped"` or `"target_unavailable"` (keeps `enabled=true`)
|
||||
4. **Provider/target starts**: `OnStackStart` (5s delay) → finds enabled integrations with non-active status → re-applies if both sides running/starting
|
||||
5. **Provider/target removed**: `OnStackRemove` → revokes and deletes integration state permanently
|
||||
6. **FileBrowser config regen**: `SyncFileBrowserMounts` regenerates `config.yaml` from scratch → `ReapplyConfigForTarget("filebrowser")` patches integration config synchronously before `docker compose up -d --force-recreate`
|
||||
|
||||
**Important**: `SyncFileBrowserMounts` uses `--force-recreate` because `config.yaml` is a bind mount — without it, `docker compose up -d` won't recreate the container when only the config file changes (compose only detects compose file changes). `ReapplyConfigForTarget` calls each handler's `Apply` with a no-op `RestartStack` since the caller handles the restart.
|
||||
|
||||
#### Built-in Handlers
|
||||
|
||||
**OnlyOffice → FileBrowser** (`onlyoffice_filebrowser.go`):
|
||||
- Apply: Reads JWT_SECRET + SUBDOMAIN from OnlyOffice app.yaml, appends `integrations.office` block to FileBrowser `config.yaml`, restarts FileBrowser
|
||||
- Apply: Reads `JWT_SECRET` + `SUBDOMAIN` from OnlyOffice app.yaml (decrypted), strips any existing `integrations:` block from FileBrowser `config.yaml` via `removeIntegrationsSection()`, appends new block with `url` (public HTTPS), `internalUrl` (`http://onlyoffice:80`), `secret`, `viewOnly: false`. Atomic write (`.tmp` + rename). Restarts FileBrowser
|
||||
- Revoke: Strips `integrations:` block from config.yaml, restarts FileBrowser
|
||||
|
||||
**OnlyOffice → Nextcloud** (`onlyoffice_nextcloud.go`):
|
||||
- Apply: Runs `docker exec -u www-data nextcloud php occ` commands (app:install, app:enable, config:app:set for DocumentServerUrl, jwt_secret)
|
||||
- Revoke: Runs `occ app:disable onlyoffice`
|
||||
- Apply: Runs `docker exec -u www-data nextcloud php occ` commands:
|
||||
1. `app:install onlyoffice` (tolerates "already installed")
|
||||
2. `app:enable onlyoffice`
|
||||
3. `config:app:set onlyoffice DocumentServerUrl --value=https://{subdomain}.{domain}`
|
||||
4. `config:app:set onlyoffice DocumentServerInternalUrl --value=http://onlyoffice:80`
|
||||
5. `config:app:set onlyoffice jwt_secret --value={JWT_SECRET}`
|
||||
6. `config:app:set onlyoffice StorageUrl --value=http://nextcloud` (internal callback URL)
|
||||
- Revoke: Runs `occ app:disable onlyoffice` (tolerates container not running / app not enabled)
|
||||
|
||||
**OnlyOffice compose template notes**: Requires Traefik middleware `X-Forwarded-Proto=https` in labels so the Document Server generates HTTPS URLs for editor resources (prevents mixed content errors in browser).
|
||||
|
||||
#### Metadata (`.felhom.yml`)
|
||||
|
||||
Provider apps declare integrations in their `.felhom.yml`. Parsed into `IntegrationDef` struct in `metadata.go`, with `HasIntegrations()` helper.
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
- target: filebrowser
|
||||
@@ -1250,15 +1272,24 @@ integrations:
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/integrations/{provider}` | List integrations for a provider app |
|
||||
| POST | `/api/integrations/{provider}/{target}` | Enable/disable integration (`{"enable": true/false}`) |
|
||||
| GET | `/api/integrations/{provider}` | List integrations for a provider app (status, target availability) |
|
||||
| POST | `/api/integrations/{provider}/{target}` | Enable/disable integration (`{"enabled": true/false}`) |
|
||||
|
||||
Routes registered **before** `hasSuffix`-based stack routes in router.go (see router bug pattern).
|
||||
|
||||
#### UI
|
||||
|
||||
Toggle switches on the provider's app info page ("Integrációk" section). Each integration shows:
|
||||
- Label and description
|
||||
Toggle switches on the provider's deploy/settings page ("Integrációk" section, within `deploy.html`). Data wired in `deployHandler()` for deployed apps only. Each integration shows:
|
||||
- Label and description from `.felhom.yml` metadata
|
||||
- Status badge: "Aktív", "Nincs telepítve", "Célalkalmazás leállítva", "Hiba"
|
||||
- Toggle checkbox (disabled when target not deployed/running)
|
||||
- JS `toggleIntegration()` → POST to API → reload on success
|
||||
|
||||
#### Wiring (main.go)
|
||||
|
||||
- `integrationStackAdapter` type implements `integrations.StackProvider` (same pattern as `stackAdapter`, `geoStackAdapter`)
|
||||
- `integrations.NewManager(sett, adapter, domain, stacksDir, encKey, logger)` — registers built-in handlers
|
||||
- Wired into API router via `SetIntegrationManager()` and web server via `SetIntegrationManager()`
|
||||
|
||||
---
|
||||
|
||||
@@ -1560,7 +1591,7 @@ Config endpoints accept session auth OR `Authorization: Bearer <hub_api_key>` (s
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/integrations/{provider}` | List integrations for provider app (status, target availability) |
|
||||
| POST | `/api/integrations/{provider}/{target}` | Enable/disable integration (`{"enable": true/false}`) |
|
||||
| POST | `/api/integrations/{provider}/{target}` | Enable/disable integration (`{"enabled": true/false}`) |
|
||||
|
||||
### Debug (debug mode only)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user