- Add CONTRIBUTING.md: interim local-git rules (no work on main; every change a feature branch merged into main) and the target state (Gitea-hosted repos, GitOps source cutover, CI/CD via Gitea Actions running the Kyverno gate + Chainsaw conformance Job, PR merge gate). Note the self-hosted-git bootstrapping circularity + off-cluster mirror/backup mitigation. - AGENTS.md: add section 12 (version-control workflow). - docs/05: source-of-truth migration (local bootstrap -> self-hosted Gitea). - docs/13 + roadmap: reprioritize Gitea as bootstrapping-critical, onboarded early (WS-APPS step 2); nextcloud onboarding split out. - task-board: T-6.1a (gitea early), T-6.1b (source+CI migration to Gitea). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
7.6 KiB
13 — Service Catalog & the ServiceDefinition
Serves: R-GITOPS-02, ties together R-NET-, R-STATE-, R-TLS-, R-DNS-, R-OBS-, R-UPD-. The catalog is the operator-facing onboarding surface.
The ServiceDefinition abstraction
Onboarding a service is adding/editing one ServiceDefinition under catalog/ in the
implementation repo. It captures intent; the platform compiles it into concrete resources
(Deployment or KubeVirt VM, PVCs, CiliumNetworkPolicies, egress routing, HTTPRoute, Certificate, DNS
record, ServiceMonitor + dashboards + alerts, and update policy). Operators declare what;
platform provides the how. This keeps every requirement wired from a single reviewable object.
Fields (canonical schema — implementers render manifests from these)
name: <string> # DNS label → <name>.home.keithjackson.dev
kind: container | vm # PaaS (default) or IaaS via KubeVirt (R-PLAT-02/03)
source:
image: <repo/image> # container: image ref
# or vmTemplate: <image/source> # vm: CDI source image
exposure: internal | public # LAN-only vs edge port-forwarded (R-DNS/R-NET)
egress: internet | vpn # egress class (R-NET-05); vpn ⇒ kill-switch (R-NET-06)
storage:
- name: config
size: <e.g. 5Gi>
class: longhorn # replicated app/config state (R-STATE-02)
- name: media # optional bulk tier (R-STATE-04)
class: bulk
dependencies: # explicit allow rules; everything else denied (R-NET-01/02)
- to: <service>
port: <n>
protocol: tcp|udp
ingressRoutes: # hostnames/paths to expose (rendered to HTTPRoute + TLS + DNS)
- host: <name>.home.keithjackson.dev
port: <servicePort>
update:
channel: stable # never pre-release unless opted in (R-UPD-01)
range: <semver range | tag regex># acceptable versions
cadence: <schedule> # how often to check/apply
autoRollbackOnFailedHealth: true # R-UPD-03
pin: <optional version> # operator freeze escape hatch
observability:
metrics: true
slis: [ up, ... ] # per-service alert intents (R-OBS-01)
backup:
policy: <ref to backup schedule> # off-cluster (R-STATE-03); or 'reacquirable' for bulk media
The schema is the contract between the operator surface and the implementation. Changing it is an interface change — coordinate per
agents/coordination-protocol.md.
Default posture for every service (unless overridden)
kind: container,exposure: internal,egress: internet.- Default-deny: no
dependencies⇒ can reach only DNS (03). - Longhorn-backed config PVC; off-cluster backup; trusted TLS; auto-DNS; monitored; auto-updated on
stable.
The initial catalog (13 services)
Egress and dependencies below are normative intent; implementers render the exact policies. "Bulk" = large media tier (04). All get trusted TLS + auto-DNS + monitoring
- auto-update unless noted.
| # | Service | kind | exposure | egress | Storage | Key dependencies (explicit allows) | Notes |
|---|---|---|---|---|---|---|---|
| 1 | plex | container | public | internet | config: longhorn (small, replicated); media: bulk | — (clients reach it; Seerr → Plex) | Largest service; media on bulk tier, metadata DB on Longhorn (04) |
| 2 | sonarr | container | internal | vpn | config: longhorn; media/downloads: bulk | → qbittorrent, → jackett; ← seerr | VPN + kill-switch (R-NET-06) |
| 3 | radarr | container | internal | vpn | config: longhorn; media/downloads: bulk | → qbittorrent, → jackett; ← seerr | VPN + kill-switch |
| 4 | jackett | container | internal | vpn | config: longhorn | ← sonarr, ← radarr | VPN + kill-switch (indexer) |
| 5 | seerr | container | public | internet | config: longhorn | → sonarr:8989, → radarr:7878, → plex:32400 | Request UI; talks to arr + Plex |
| 6 | pihole | container | internal | internet | config: longhorn | (provides DNS to cluster/LAN) | Also a platform DNS dependency (09) |
| 7 | nextcloud | container | public | internet | data: longhorn (irreplaceable → backed up) | → its DB | File sync; DR-critical data |
| 8 | minecraft | container | public | internet | world: longhorn (backed up) | — | Exposes game port via edge |
| 9 | unifi | container | internal | internet | config: longhorn | — | UniFi network controller |
| 10 | semaphore | container | internal | internet | config/db: longhorn | — | Ansible UI |
| 11 | kasm | container | internal | internet | config: longhorn | — | Kasm Workspaces (may need more resources; container-first, VM if required) |
| 12 | gitea | container | public | internet | repos/db: longhorn (irreplaceable → backed up) | — | Git hosting; bootstrapping-critical — self-hosts the platform's GitOps source + CI/CD; onboarded early (see below); DR-critical (off-cluster backup + mirror) |
| 13 | qbittorrent | container | internal | vpn | config: longhorn; downloads: bulk | ← sonarr, ← radarr | VPN + kill-switch; download client |
VPN-class set (R-NET-05/06): sonarr, radarr, jackett, qbittorrent — all egress via VPN with an enforced kill-switch. Their inter-dependencies (arr → qbittorrent/jackett) are east-west allows within the deny-by-default fabric; being VPN-class governs egress, not intra-cluster reachability.
Dependency graph (explicit allows only; all else denied):
seerr → sonarr, radarr, plex
sonarr → qbittorrent, jackett
radarr → qbittorrent, jackett
(everything → DNS only, by default)
"…but not limited to": the catalog is open-ended. New services follow the same ServiceDefinition and inherit all guarantees automatically — that is the point of the abstraction.
Cross-cutting notes
- Plex media is the R-STATE-04 case: keep the metadata DB on replicated Longhorn; keep the large library on the bulk tier; downloads stage on bulk and are handed off by the arr stack.
- DR-critical data (Nextcloud, Gitea, Minecraft worlds, PiHole config, service DBs) MUST have
off-cluster backups (04); bulk media may be
reacquirableper operator choice (OQ-2). - Public exposure (Plex, Seerr, Nextcloud, Minecraft, Gitea) requires edge port-forwarding
(09); everything else stays
internal. - Kasm is resource-heavy and may be a candidate for
kind: vmif container mode proves insufficient — the ServiceDefinition makes that a one-field change. - Gitea is bootstrapping-critical. The platform self-hosts its git and CI/CD, so Gitea is brought
online early (WS-APPS step 2, right after PiHole). Once stable, the GitOps source and CI move
from the local bootstrap source to Gitea + Gitea Actions (05,
../CONTRIBUTING.md). Because a cluster loss would take its own git host with it, Gitea's repos/DB are backed up off-cluster and mirrored, so the platform can always be rebuilt from a source that outlives the cluster.
Acceptance gates
- Each service is fully wired from its ServiceDefinition alone (compute, storage, policy, egress, TLS, DNS, monitoring, updates) — no manual steps beyond merge.
- Default-deny holds; only listed dependencies work.
- VPN-class services egress via VPN with a proven kill-switch.
- Stateful services survive destroy/rebuild; DR-critical data restores from off-cluster backup.