Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,17 @@ prerequisite (Longhorn); the rest are convenience + robustness.
merge/webhook today — B1–B3; PR-preview was deferred to Argo). Enhancement.

### J. App catalog / examples
- [ ] **J1 — example manifests** for the target apps under `examples/`
(Ollama + Open WebUI on GPU, Jellyfin on NFS, MinIO, Authentik-as-SSO, a
static blog behind the tunnel) — the deploy-today path, documented
end-to-end. Not a code gap; lowers the activation energy.
- [x] **J1 — example manifests** under `examples/homelab/` — the full stack
end-to-end: a GPU-capable k3s `Cluster` (`.cue`, `openctl validate`-checked
offline) with a GPU pool + a Longhorn-prepped storage pool (`nodePrep:
open-iscsi`); a `Platform` (traefik/cloudflared/longhorn/nfs/nvidia + a
generic `kube-prometheus-stack`); a Cloudflare `Tunnel` + `DNSRecord`s that
`$ref` its `cnameTarget`; and `HelmRelease`s for Ollama+Open WebUI (GPU),
Jellyfin (NFS), MinIO, Authentik (SSO). README walks the deploy-today path
and the three wiring mechanisms (`$ref` outputs, `$secret` credentials,
`nodePrep` prerequisites). Not a code gap; lowers activation energy. The
`.cue` cluster validates against the compiled-in schema; the plugin-kind
YAML is well-formed and schema-accurate.

## Architecture & consolidation (2026-07-13)

Expand Down
73 changes: 73 additions & 0 deletions examples/homelab/01-cluster.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Homelab k3s cluster: an HA-ish control plane plus two purpose-built worker
// pools — a GPU pool for local models (Ollama/Open WebUI) and a storage pool
// prepped for Longhorn (open-iscsi installed via nodePrep).
//
// Validate this file with: openctl validate -f examples/homelab/01-cluster.cue
//
// The cluster publishes status.outputs.kubeconfigPath, which the workload
// manifests (Platform, HelmReleases) $ref to target this cluster — that ref
// also DAG-orders them after the cluster is Ready.
import "openctl.io/schemas/k3s"

k3s.#Cluster & {
metadata: name: "home"
spec: {
compute: {
provider: "proxmox"
// A cloud image is downloaded once into a template; nodes clone it.
image: {
url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
storage: "local" // where the downloaded image + snippets live
diskStorage: "local-lvm" // where node disks are allocated
}
default: {cpus: 2, memoryMB: 4096, diskGB: 40}
}
nodes: {
controlPlane: count: 1

workers: [
// General-purpose workloads (blogs, MinIO, Authentik, Jellyfin).
{name: "general", count: 2, size: {cpus: 4, memoryMB: 8192, diskGB: 60}},

// GPU pool for a local model. Pinned to the host with the card;
// the pool's VMs get q35 + OVMF + the GPU passed through. Pair
// with the nvidiaDevicePlugin Platform component so workloads can
// request nvidia.com/gpu.
{
name: "gpu"
count: 1
nodes: ["pve-gpu"]
size: {cpus: 8, memoryMB: 24576, diskGB: 80}
gpu: {
efiStorage: "local-lvm"
devices: [
{mapping: "rtx4090", primaryGPU: true},
]
}
},

// Storage pool for Longhorn replicated block storage. nodePrep
// installs open-iscsi (Longhorn's node prerequisite) on first
// boot via cloud-init.
{
name: "storage"
count: 3
size: {cpus: 4, memoryMB: 8192, diskGB: 200}
nodePrep: {
packages: ["open-iscsi"]
runcmd: ["systemctl enable --now iscsid"]
}
},
]
}
network: staticIPs: {
startIP: "192.168.1.100"
gateway: "192.168.1.1"
netmask: "24"
}
ssh: {
privateKeyPath: "~/.ssh/id_ed25519"
publicKeys: ["ssh-ed25519 AAAA... you@homelab"]
}
}
}
69 changes: 69 additions & 0 deletions examples/homelab/02-platform.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Platform: the opt-in infra layer for the cluster — ingress, tunnel, storage
# classes, GPU scheduling. Each enabled component installs one Helm release.
# Nothing is enabled by default; this is the homelab's chosen set.
#
# The kubeconfigPath $ref targets the k3s Cluster from 01-cluster.cue: openctl
# resolves it to the on-disk kubeconfig path AND orders this apply after the
# cluster is Ready.
apiVersion: k8s.openctl.io/v1
kind: Platform
metadata:
name: home
spec:
kubeconfigPath:
$ref:
apiVersion: k3s.openctl.io/v1
kind: Cluster
name: home
field: status.outputs.kubeconfigPath

# --- Named presets (default chart coords; enable with a flag) ---

# Ingress controller.
traefik:
enabled: true

# Cloudflare Tunnel connector. Its run token is pulled straight from the
# Tunnel's get-token action via the `action` $secret provider — no manual
# token copying, and only the marker is persisted (never the token).
cloudflared:
enabled: true
values:
cloudflare:
tunnel_token:
$secret:
provider: action
key: "cloudflare.openctl.io/v1/Tunnel/home#get-token"

# Replicated block storage. The storage worker pool already has open-iscsi
# (see 01-cluster.cue nodePrep), which Longhorn requires.
longhorn:
enabled: true
values:
defaultSettings:
defaultReplicaCount: 2

# Dynamic NFS-backed StorageClass off the Synology (media, Home Assistant).
nfsProvisioner:
enabled: true
values:
nfs:
server: "192.168.1.10"
path: "/volume1/k8s"

# Advertise nvidia.com/gpu on the GPU pool so Ollama can request one.
nvidiaDevicePlugin:
enabled: true

# --- Generic components: any chart, no code change (K4a) ---
# These install exactly like the presets but aren't baked into openctl.
components:
kube-prometheus-stack:
chart:
repo: https://prometheus-community.github.io/helm-charts
name: kube-prometheus-stack
version: "65.0.0"
namespace: monitoring
values:
grafana:
enabled: true
23 changes: 23 additions & 0 deletions examples/homelab/03-tunnel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Cloudflare Tunnel: one connector fronting every public app. Ingress rules map
# public hostnames to in-cluster Traefik (or a service directly). cloudflared
# (installed by the Platform) runs the connector using this tunnel's token.
#
# A catch-all 404 is appended automatically after the last host-scoped rule.
apiVersion: cloudflare.openctl.io/v1
kind: Tunnel
metadata:
name: home
spec:
# accountId optional if the provider config sets defaults.accountId.
ingress:
# Everything routes through Traefik, which does host-based routing in-cluster.
- hostname: "chat.example.com"
service: "https://traefik.traefik.svc.cluster.local:443"
- hostname: "jellyfin.example.com"
service: "https://traefik.traefik.svc.cluster.local:443"
- hostname: "minio.example.com"
service: "https://traefik.traefik.svc.cluster.local:443"
- hostname: "auth.example.com"
service: "https://traefik.traefik.svc.cluster.local:443"
- hostname: "blog.example.com"
service: "https://traefik.traefik.svc.cluster.local:443"
50 changes: 50 additions & 0 deletions examples/homelab/04-dns.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Public DNS: one CNAME per app, each pointing at the Tunnel. Instead of
# hand-copying "<tunnel-id>.cfargotunnel.com", the content $refs the Tunnel's
# status.cnameTarget — openctl resolves it at apply time (and orders each
# record after the Tunnel). Change the tunnel and the records follow.
#
# One document per record (YAML --- separated).
apiVersion: cloudflare.openctl.io/v1
kind: DNSRecord
metadata:
name: chat
spec:
type: CNAME
name: "chat.example.com"
proxied: true
content:
$ref:
apiVersion: cloudflare.openctl.io/v1
kind: Tunnel
name: home
field: status.cnameTarget
---
apiVersion: cloudflare.openctl.io/v1
kind: DNSRecord
metadata:
name: jellyfin
spec:
type: CNAME
name: "jellyfin.example.com"
proxied: true
content:
$ref:
apiVersion: cloudflare.openctl.io/v1
kind: Tunnel
name: home
field: status.cnameTarget
---
apiVersion: cloudflare.openctl.io/v1
kind: DNSRecord
metadata:
name: blog
spec:
type: CNAME
name: "blog.example.com"
proxied: true
content:
$ref:
apiVersion: cloudflare.openctl.io/v1
kind: Tunnel
name: home
field: status.cnameTarget
69 changes: 69 additions & 0 deletions examples/homelab/05-ollama-openwebui.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Local model stack: Ollama (the model server, scheduled onto the GPU pool via
# nvidia.com/gpu) + Open WebUI (the chat front-end, exposed at chat.example.com
# through the Tunnel). Both are stock Helm charts — openctl just installs them
# onto the cluster the kubeconfigPath $ref points at.
apiVersion: k8s.openctl.io/v1
kind: HelmRelease
metadata:
name: ollama
spec:
kubeconfigPath:
$ref:
apiVersion: k3s.openctl.io/v1
kind: Cluster
name: home
field: status.outputs.kubeconfigPath
namespace: ai
createNamespace: true
chart:
repo: https://otwld.github.io/ollama-helm
name: ollama
version: "1.30.0"
values:
ollama:
gpu:
enabled: true
type: nvidia
number: 1
models:
pull:
- llama3.2
# Request the GPU advertised by the nvidiaDevicePlugin Platform component;
# nodeSelector/tolerations pin it to the GPU pool if you label those nodes.
resources:
limits:
nvidia.com/gpu: 1
persistentVolume:
enabled: true
storageClass: longhorn
size: 50Gi
---
apiVersion: k8s.openctl.io/v1
kind: HelmRelease
metadata:
name: open-webui
spec:
kubeconfigPath:
$ref:
apiVersion: k3s.openctl.io/v1
kind: Cluster
name: home
field: status.outputs.kubeconfigPath
namespace: ai
createNamespace: true
chart:
repo: https://helm.openwebui.com
name: open-webui
version: "5.4.0"
values:
# Point the UI at the in-cluster Ollama service instead of bundling one.
ollama:
enabled: false
ollamaUrls:
- http://ollama.ai.svc.cluster.local:11434
persistence:
enabled: true
storageClass: longhorn
ingress:
enabled: true
host: chat.example.com
Loading
Loading