Parallel analysis of tech stack, architecture, structure, conventions, testing patterns, integrations, and concerns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
9.2 KiB
Markdown
256 lines
9.2 KiB
Markdown
# External Integrations
|
|
|
|
**Analysis Date:** 2026-03-23
|
|
|
|
## APIs & External Services
|
|
|
|
**DIUN (Docker Image Update Notifier):**
|
|
- DIUN sends webhook POST requests when container image updates are detected
|
|
- Endpoint: `POST /webhook`
|
|
- SDK/Client: None (DIUN pushes to this app; this app is the receiver)
|
|
- Auth: `Authorization` header must match `WEBHOOK_SECRET` env var (when set)
|
|
- Source: `pkg/diunwebhook/diunwebhook.go` lines 163-199
|
|
|
|
## API Contracts
|
|
|
|
### Webhook Ingestion
|
|
|
|
**`POST /webhook`** - Receive a DIUN event
|
|
- Handler: `WebhookHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Auth: `Authorization` header checked via constant-time compare against `WEBHOOK_SECRET`
|
|
- Request body:
|
|
```json
|
|
{
|
|
"diun_version": "4.28.0",
|
|
"hostname": "docker-host",
|
|
"status": "new",
|
|
"provider": "docker",
|
|
"image": "registry/org/image:tag",
|
|
"hub_link": "https://hub.docker.com/r/...",
|
|
"mime_type": "application/vnd.docker.distribution.manifest.v2+json",
|
|
"digest": "sha256:abc123...",
|
|
"created": "2026-03-23T10:00:00Z",
|
|
"platform": "linux/amd64",
|
|
"metadata": {
|
|
"ctn_names": "container-name",
|
|
"ctn_id": "abc123",
|
|
"ctn_state": "running",
|
|
"ctn_status": "Up 2 days"
|
|
}
|
|
}
|
|
```
|
|
- Response: `200 OK` (empty body) on success
|
|
- Errors: `401 Unauthorized`, `405 Method Not Allowed`, `400 Bad Request` (missing `image` field or invalid JSON), `500 Internal Server Error`
|
|
- Behavior: Upserts into `updates` table keyed by `image`. Replaces existing entry and resets `acknowledged_at` to NULL.
|
|
|
|
### Updates API
|
|
|
|
**`GET /api/updates`** - List all tracked image updates
|
|
- Handler: `UpdatesHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Response: `200 OK` with JSON object keyed by image name:
|
|
```json
|
|
{
|
|
"registry/org/image:tag": {
|
|
"event": { /* DiunEvent fields */ },
|
|
"received_at": "2026-03-23T10:00:00Z",
|
|
"acknowledged": false,
|
|
"tag": { "id": 1, "name": "production" } // or null
|
|
}
|
|
}
|
|
```
|
|
|
|
**`PATCH /api/updates/{image}`** - Dismiss (acknowledge) an update
|
|
- Handler: `DismissHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- URL parameter: `{image}` is the full image name (URL-encoded)
|
|
- Response: `204 No Content` on success
|
|
- Errors: `405 Method Not Allowed`, `400 Bad Request`, `404 Not Found`, `500 Internal Server Error`
|
|
- Behavior: Sets `acknowledged_at = datetime('now')` on the matching row
|
|
|
|
### Tags API
|
|
|
|
**`GET /api/tags`** - List all tags
|
|
- Handler: `TagsHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Response: `200 OK` with JSON array:
|
|
```json
|
|
[{ "id": 1, "name": "production" }, { "id": 2, "name": "staging" }]
|
|
```
|
|
|
|
**`POST /api/tags`** - Create a new tag
|
|
- Handler: `TagsHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Request body: `{ "name": "production" }`
|
|
- Response: `201 Created` with `{ "id": 1, "name": "production" }`
|
|
- Errors: `400 Bad Request` (empty name), `409 Conflict` (duplicate name), `500 Internal Server Error`
|
|
|
|
**`DELETE /api/tags/{id}`** - Delete a tag
|
|
- Handler: `TagByIDHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- URL parameter: `{id}` is integer tag ID
|
|
- Response: `204 No Content`
|
|
- Errors: `405 Method Not Allowed`, `400 Bad Request` (invalid ID), `404 Not Found`, `500 Internal Server Error`
|
|
- Behavior: Cascading delete removes all `tag_assignments` referencing this tag
|
|
|
|
### Tag Assignments API
|
|
|
|
**`PUT /api/tag-assignments`** - Assign an image to a tag
|
|
- Handler: `TagAssignmentHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Request body: `{ "image": "registry/org/image:tag", "tag_id": 1 }`
|
|
- Response: `204 No Content`
|
|
- Errors: `400 Bad Request`, `404 Not Found` (tag doesn't exist), `500 Internal Server Error`
|
|
- Behavior: `INSERT OR REPLACE` - reassigns if already assigned
|
|
|
|
**`DELETE /api/tag-assignments`** - Unassign an image from its tag
|
|
- Handler: `TagAssignmentHandler` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Request body: `{ "image": "registry/org/image:tag" }`
|
|
- Response: `204 No Content`
|
|
- Errors: `400 Bad Request`, `500 Internal Server Error`
|
|
|
|
### Static File Serving
|
|
|
|
**`GET /` and all unmatched routes** - Serve React SPA
|
|
- Handler: `http.FileServer(http.Dir("./frontend/dist"))` in `cmd/diunwebhook/main.go`
|
|
- Serves the production build of the React frontend
|
|
|
|
## Data Storage
|
|
|
|
**Database:**
|
|
- SQLite (file-based, single-writer)
|
|
- Connection: `DB_PATH` env var (default `./diun.db`)
|
|
- Driver: `modernc.org/sqlite` (pure Go, registered as `"sqlite"` in `database/sql`)
|
|
- Max open connections: 1 (`db.SetMaxOpenConns(1)`)
|
|
- Write concurrency: `sync.Mutex` in `pkg/diunwebhook/diunwebhook.go`
|
|
|
|
**Schema:**
|
|
|
|
```sql
|
|
-- Table: updates (one row per unique image)
|
|
CREATE TABLE IF NOT EXISTS updates (
|
|
image TEXT PRIMARY KEY,
|
|
diun_version TEXT NOT NULL DEFAULT '',
|
|
hostname TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
hub_link TEXT NOT NULL DEFAULT '',
|
|
mime_type TEXT NOT NULL DEFAULT '',
|
|
digest TEXT NOT NULL DEFAULT '',
|
|
created TEXT NOT NULL DEFAULT '',
|
|
platform TEXT NOT NULL DEFAULT '',
|
|
ctn_name TEXT NOT NULL DEFAULT '',
|
|
ctn_id TEXT NOT NULL DEFAULT '',
|
|
ctn_state TEXT NOT NULL DEFAULT '',
|
|
ctn_status TEXT NOT NULL DEFAULT '',
|
|
received_at TEXT NOT NULL,
|
|
acknowledged_at TEXT
|
|
);
|
|
|
|
-- Table: tags (user-defined grouping labels)
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE
|
|
);
|
|
|
|
-- Table: tag_assignments (image-to-tag mapping, one tag per image)
|
|
CREATE TABLE IF NOT EXISTS tag_assignments (
|
|
image TEXT PRIMARY KEY,
|
|
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE
|
|
);
|
|
```
|
|
|
|
**Migrations:**
|
|
- Schema is created on startup via `InitDB()` in `pkg/diunwebhook/diunwebhook.go`
|
|
- Uses `CREATE TABLE IF NOT EXISTS` for all tables
|
|
- One manual migration: `ALTER TABLE updates ADD COLUMN acknowledged_at TEXT` (silently ignored if already present)
|
|
- No formal migration framework; migrations are inline Go code
|
|
|
|
**File Storage:** Local filesystem only (SQLite database file)
|
|
|
|
**Caching:** None
|
|
|
|
## Authentication & Identity
|
|
|
|
**Webhook Authentication:**
|
|
- Token-based via `WEBHOOK_SECRET` env var
|
|
- Checked in `WebhookHandler` using `crypto/subtle.ConstantTimeCompare` against the `Authorization` header
|
|
- When `WEBHOOK_SECRET` is empty, the webhook endpoint is unprotected (warning logged at startup)
|
|
- Implementation: `pkg/diunwebhook/diunwebhook.go` lines 54-56, 163-170
|
|
|
|
**User Authentication:** None. The dashboard and all API endpoints (except webhook) are open/unauthenticated.
|
|
|
|
## Monitoring & Observability
|
|
|
|
**Error Tracking:** None (no Sentry, Datadog, etc.)
|
|
|
|
**Logs:**
|
|
- Go stdlib `log` package writing to stdout
|
|
- Key log points: startup warnings, webhook receipt, errors in handlers
|
|
- No structured logging framework
|
|
|
|
## CI/CD & Deployment
|
|
|
|
**Hosting:** Self-hosted via Docker on a Gitea instance at `gitea.jeanlucmakiola.de`
|
|
|
|
**Container Registry:** `gitea.jeanlucmakiola.de/makiolaj/diundashboard`
|
|
|
|
**CI Pipeline (Gitea Actions):**
|
|
- Config: `.gitea/workflows/ci.yml`
|
|
- Triggers: Push to `develop`, PRs targeting `develop`
|
|
- Steps: `gofmt` check, `go vet`, tests with coverage (warn below 80%), `go build`
|
|
- Runner: Custom Docker image with Go + Node/Bun toolchains
|
|
|
|
**Release Pipeline (Gitea Actions):**
|
|
- Config: `.gitea/workflows/release.yml`
|
|
- Trigger: Manual `workflow_dispatch` with semver bump choice (patch/minor/major)
|
|
- Steps: Run full CI checks, compute new version tag, create git tag, build and push Docker image (versioned + `latest`), create Gitea release with changelog
|
|
- Secrets required: `GITEA_TOKEN`, `REGISTRY_TOKEN`
|
|
|
|
**Docker Build:**
|
|
- Multi-stage Dockerfile at project root (`Dockerfile`)
|
|
- Stage 1: `oven/bun:1-alpine` - Build frontend (`bun install --frozen-lockfile && bun run build`)
|
|
- Stage 2: `golang:1.26-alpine` - Build Go binary (`CGO_ENABLED=0 go build`)
|
|
- Stage 3: `alpine:3.18` - Runtime with binary + static assets, exposes port 8080
|
|
|
|
**Docker Compose:**
|
|
- `compose.yml` - Production deploy (pulls `latest` from registry, mounts `diun-data` volume at `/data`)
|
|
- `compose.dev.yml` - Local development (builds from Dockerfile)
|
|
|
|
**Documentation Site:**
|
|
- Separate `docs/Dockerfile` and `docs/nginx.conf` for static site deployment via Nginx
|
|
- Built with VitePress, served as static HTML
|
|
|
|
## Environment Configuration
|
|
|
|
**Required env vars (production):**
|
|
- None strictly required (all have defaults)
|
|
|
|
**Recommended env vars:**
|
|
- `WEBHOOK_SECRET` - Protect webhook endpoint from unauthorized access
|
|
- `DB_PATH` - Set to `/data/diun.db` in Docker for persistent volume mount
|
|
- `PORT` - Override default port 8080
|
|
|
|
**Secrets:**
|
|
- `WEBHOOK_SECRET` - Shared secret between DIUN and this app
|
|
- `GITEA_TOKEN` - CI/CD pipeline (Gitea API access)
|
|
- `REGISTRY_TOKEN` - CI/CD pipeline (Docker registry push)
|
|
|
|
## Webhooks & Callbacks
|
|
|
|
**Incoming:**
|
|
- `POST /webhook` - Receives DIUN image update notifications
|
|
|
|
**Outgoing:**
|
|
- None
|
|
|
|
## Frontend-Backend Communication
|
|
|
|
**Dev Mode:**
|
|
- Vite dev server on `:5173` proxies `/api` and `/webhook` to `http://localhost:8080` (`frontend/vite.config.ts`)
|
|
|
|
**Production:**
|
|
- Go server serves `frontend/dist/` at `/` via `http.FileServer`
|
|
- API and webhook routes are on the same origin (no CORS needed)
|
|
|
|
**Polling:**
|
|
- React SPA polls `GET /api/updates` every 5 seconds (no WebSocket/SSE)
|
|
|
|
---
|
|
|
|
*Integration audit: 2026-03-23*
|