diff --git a/cluster-config/cilium-values.yaml b/cluster-config/cilium-values.yaml new file mode 100644 index 0000000..08ee44a --- /dev/null +++ b/cluster-config/cilium-values.yaml @@ -0,0 +1,86 @@ +# cluster-config/cilium-values.yaml +# Cilium CNI — installed via talosctl (not helmfile) during cluster bootstrap. +# Applied once: `helm install cilium cilium/cilium -n kube-system -f cilium-values.yaml` +# +# Why Cilium: Talos Linux does not ship kube-proxy. Cilium's eBPF dataplane +# replaces it entirely (kubeProxyReplacement: true) and also handles LB-IPAM +# so LoadBalancer services get real IPs without MetalLB. + +# ── cgroup ──────────────────────────────────────────────────────────────────── +# Talos mounts cgroups at boot before any container runtime starts. +# autoMount: false tells Cilium to use the existing mount rather than trying +# to mount its own — double-mounting on Talos causes init failures. +cgroup: + autoMount: + enabled: false + hostRoot: /sys/fs/cgroup # where Talos exposes the cgroup v2 hierarchy + +# ── IPAM ────────────────────────────────────────────────────────────────────── +# kubernetes mode: Cilium allocates pod IPs from the pod CIDR that Talos +# configured for each node (--pod-cidr in the kubelet). Alternative is +# Cilium's own cluster-pool IPAM, but that requires extra config and +# conflicts with the Talos node CIDR assignment. +ipam: + mode: kubernetes + +# ── Operator ────────────────────────────────────────────────────────────────── +# Single replica is fine for a 3-node homelab. The operator manages CiliumNode +# objects and LB-IPAM pools — it does not sit in the data path. +operator: + replicas: 1 + +# ── kube-proxy replacement ──────────────────────────────────────────────────── +# Talos is deliberately installed without kube-proxy (machineConfig +# install.extensions excludes it). Cilium must replace it completely — +# partial replacement would leave Service ClusterIPs unreachable. +kubeProxyReplacement: true + +# ── L2 announcements ────────────────────────────────────────────────────────── +# Without this, LB-IPAM (k8s/cilium/lb-ipam-pool.yaml) assigns real IPs to +# LoadBalancer Services, but nothing ARPs for them on the LAN — the IP shows +# up in `kubectl get svc` but is 100% unreachable from outside the cluster +# (confirmed: forgejo's .165 and shadowsocks' .166 both had incomplete ARP +# entries and 100% ping loss before this). This flag is what actually makes +# k8s/cilium/l2-announcement-policy.yaml take effect instead of being inert. +l2announcements: + enabled: true + +# ── API server endpoint ─────────────────────────────────────────────────────── +# Cilium needs to talk to the Kubernetes API to watch Nodes/Services/Endpoints. +# On Talos the API server listens on 127.0.0.1:7445 locally (the external +# port 6443 requires the node's external cert, which may not be available +# during early bootstrap). This is the standard Talos Cilium bootstrap config. +k8sServiceHost: 127.0.0.1 +k8sServicePort: 7445 + +# ── Security context / capabilities ────────────────────────────────────────── +# Cilium's eBPF programs run in the kernel and require elevated capabilities. +# These are the minimum set needed — removing any of them breaks networking. +# +# NET_ADMIN / NET_RAW — manipulate iptables/nftables and raw sockets +# IPC_LOCK — lock eBPF maps in memory (prevents paging out BPF state) +# SYS_ADMIN — call bpf() syscall and mount BPF filesystem +# SYS_RESOURCE — raise RLIMIT_MEMLOCK for BPF map memory +# DAC_OVERRIDE / FOWNER / SETGID / SETUID — file permission ops during init +# CHOWN / KILL — container lifecycle management +# +# cleanCiliumState runs as a one-shot init container to wipe stale eBPF state +# on upgrades — it needs NET_ADMIN, SYS_ADMIN, SYS_RESOURCE only. +securityContext: + capabilities: + ciliumAgent: + - CHOWN + - KILL + - NET_ADMIN + - NET_RAW + - IPC_LOCK + - SYS_ADMIN + - SYS_RESOURCE + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + cleanCiliumState: + - NET_ADMIN + - SYS_ADMIN + - SYS_RESOURCE diff --git a/cluster-config/longhorn_bootstrap.sh b/cluster-config/longhorn_bootstrap.sh new file mode 100644 index 0000000..fa7f056 --- /dev/null +++ b/cluster-config/longhorn_bootstrap.sh @@ -0,0 +1,19 @@ +# bootstrap.sh +#!/bin/bash + +# Wait for cluster to be ready +kubectl wait --for=condition=Ready nodes --all --timeout=300s + +# Longhorn requires privileged pods and hostPath volumes +kubectl create namespace longhorn-system --dry-run=client -o yaml | kubectl apply -f - +kubectl label namespace longhorn-system \ + pod-security.kubernetes.io/enforce=privileged \ + pod-security.kubernetes.io/enforce-version=latest \ + --overwrite + +# Install Longhorn +kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.7.0/deploy/longhorn.yaml + +# Set as default StorageClass +kubectl patch storageclass longhorn \ + -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' diff --git a/helmfile.yaml.gotmpl b/helmfile.yaml.gotmpl new file mode 100644 index 0000000..137645f --- /dev/null +++ b/helmfile.yaml.gotmpl @@ -0,0 +1,971 @@ +# helmfile.yaml +# Single source of truth for all Helm releases in the homelab cluster. +# +# Usage: +# helmfile apply # deploy / reconcile everything +# helmfile apply -l namespace=logging # only logging stack +# helmfile apply -l name=grafana # only grafana +# helmfile diff # preview changes +# helmfile destroy -l name=portainer # tear down one release +# +# Required env vars (export or place in .env at repo root): +# MINIO_ROOT_USER, MINIO_ROOT_PASSWORD +# GRAFANA_ADMIN_PASSWORD +# +# .env is loaded automatically by helmfile if it exists at the repo root. + +repositories: + - name: grafana + url: https://grafana.github.io/helm-charts + - name: minio + url: https://charts.min.io + - name: prometheus-community + url: https://prometheus-community.github.io/helm-charts + - name: portainer + url: https://portainer.github.io/k8s/ + - name: authentik + url: https://charts.goauthentik.io + - name: hashicorp + url: https://helm.releases.hashicorp.com + - name: ingress-nginx + url: https://kubernetes.github.io/ingress-nginx + - name: argo + url: https://argoproj.github.io/argo-helm + - name: gitea-charts + url: https://dl.gitea.com/charts/ + - name: jetstack + url: https://charts.jetstack.io + - name: stakater + url: https://stakater.github.io/stakater-charts + - name: strimzi + url: https://strimzi.io/charts/ + - name: bitnami + url: https://charts.bitnami.com/bitnami + - name: temporal + url: https://go.temporal.io/helm-charts + - name: cnpg + url: https://cloudnative-pg.github.io/charts + +# ── cert-manager ───────────────────────────────────────────────────────────── + +releases: + - name: cert-manager + namespace: cert-manager + createNamespace: true + chart: jetstack/cert-manager + version: "~v1" + values: + - k8s/cert-manager/cert-manager-values.yaml + set: + # CRDs must be installed by the chart — avoids a separate kubectl apply step + - name: crds.enabled + value: true + hooks: + - events: ["postsync"] + command: bash + args: + - -c + - | + # Wait for cert-manager webhooks to be ready before applying CRD instances. + # Without this, ClusterIssuer/Certificate creation races the webhook and fails. + kubectl rollout status deploy/cert-manager -n cert-manager --timeout=120s + kubectl rollout status deploy/cert-manager-webhook -n cert-manager --timeout=120s + + kubectl apply -f - <<'EOF' + # Phase 2a — bootstrap issuer (selfSigned) used only to sign the CA cert. + # Never referenced by ingresses — its sole job is to sign homelab-ca below. + apiVersion: cert-manager.io/v1 + kind: ClusterIssuer + metadata: + name: selfsigned-bootstrap + spec: + selfSigned: {} + --- + # Phase 2b — the cluster CA certificate. + # cert-manager stores the signed cert + key in homelab-ca-secret. + # isCA: true marks it so it can sign other certs. + # 10-year lifetime; renewBefore triggers 30 days early. + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: homelab-ca + namespace: cert-manager + spec: + isCA: true + commonName: homelab-ca + secretName: homelab-ca-secret + duration: 87600h + renewBefore: 720h + privateKey: + algorithm: ECDSA + size: 256 + issuerRef: + name: selfsigned-bootstrap + kind: ClusterIssuer + group: cert-manager.io + --- + # Phase 2c — the real issuer all ingresses reference. + # Reads the CA cert+key from homelab-ca-secret and signs per-hostname certs. + # Annotate any ingress with: cert-manager.io/cluster-issuer: homelab-ca + apiVersion: cert-manager.io/v1 + kind: ClusterIssuer + metadata: + name: homelab-ca + spec: + ca: + secretName: homelab-ca-secret + EOF + +# ── Stakater Reloader ───────────────────────────────────────────────────────── +# Watches Deployments for secret.reloader.stakater.com/reload and +# configmap.reloader.stakater.com/reload annotations, then triggers a rolling +# restart whenever the named Secret or ConfigMap is updated (e.g. cert-manager +# renewing a TLS cert, or homelab-ca rotating). This replaces the need for +# manual `kubectl rollout restart` on cert renewal. + + - name: reloader + namespace: reloader + createNamespace: true + chart: stakater/reloader + version: "~1" + + # Pod Disruption Budgets applied after reloader (ensures all namespaces exist) + hooks: + - events: ["postsync"] + command: bash + args: + - -c + - kubectl apply -f k8s/base/poddisruptionbudgets.yaml + +# ── Ingress ─────────────────────────────────────────────────────────────────── + + - name: ingress-nginx + namespace: ingress-nginx + createNamespace: true + chart: ingress-nginx/ingress-nginx + values: + - k8s/ingress/nginx-values.yaml + # ServiceMonitor enabled in nginx-values.yaml requires the Prometheus Operator + # CRDs, which the prometheus release installs — must apply after it. + needs: + - monitoring/prometheus + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh ingress-nginx + # LB-IPAM pool must exist before any LoadBalancer service is created, + # otherwise services stay . Apply it here as the first hook. + kubectl apply -f k8s/cilium/lb-ipam-pool.yaml + kubectl apply -f k8s/coredns/coredns-configmap.yaml + kubectl rollout restart deployment/coredns -n kube-system + kubectl rollout status deployment/coredns -n kube-system --timeout=60s + # Wildcard TLS cert — must exist before nginx starts so it can read the secret. + # cert-manager issues it in the ingress-nginx namespace; wait until Ready. + kubectl apply -f k8s/ingress/wildcard-cert.yaml + kubectl wait certificate homelab-tls -n ingress-nginx \ + --for=condition=Ready --timeout=120s + - events: ["postsync"] + command: kubectl + args: + - apply + - -f + - k8s/ingress/ingress.yaml + +# ── CloudNativePG (centralized database) ────────────────────────────────────── +# Single HA cluster (1 primary + 2 replicas) serving Authentik + story-crater-backend. +# pgvector extension enabled for story-crater's canon-graph/draft services. + - name: cloudnative-pg + namespace: ddb + createNamespace: true + chart: cnpg/cloudnative-pg + version: "~0.20" + values: + - k8s/ddb/cnpg-values.yaml + needs: + - ingress-nginx/ingress-nginx # wait for ingress-nginx to be ready + hooks: + - events: ["postsync"] + command: bash + args: + - -c + - | + # Wait for operator to be ready + kubectl rollout status deploy/cloudnative-pg -n ddb --timeout=120s + + # Apply the CNPG Cluster CR with pgvector support + # Note: Using unquoted EOF so bash can expand variables + kubectl apply -f - </dev/null || true + kubectl wait pod -n ddb -l cnpg.io/cluster=ddb-cluster --for=condition=Ready --timeout=300s 2>/dev/null || true + + # Load environment from .env file (safe variable expansion) + set -a + [ -f .env ] && source .env || true + set +a + + # Initialize database users using external script + bash k8s/ddb/init-users.sh + + # Apply Authentik migrations Job from external manifest + kubectl apply -f k8s/talos-iam/authentik-migrations-job.yaml + + # Wait for migrations Job to complete before proceeding + kubectl wait --for=condition=complete job/authentik-migrations -n iam --timeout=300s 2>/dev/null || true + +# ── IAM (Authentik + Vault) ─────────────────────────────────────────────────── + - name: authentik + namespace: iam + createNamespace: true + chart: authentik/authentik + values: + - k8s/talos-iam/authentik-values.yaml + # server/worker metrics.serviceMonitor.enabled requires the Prometheus Operator CRDs. + # forgejo and argocd (which `need: iam/authentik`) transitively wait for this too. + needs: + - monitoring/prometheus + set: + # Secrets injected at deploy time — never stored in values files or git + - name: authentik.secret_key + value: {{ env "AUTHENTIK_SECRET_KEY" }} + - name: authentik.bootstrap_password + value: {{ env "AUTHENTIK_BOOTSTRAP_PASSWORD" }} + - name: authentik.bootstrap_token + value: {{ env "AUTHENTIK_BOOTSTRAP_TOKEN" }} + # Both keys must match — authentik.postgresql.password is what the app + # uses to connect; postgresql.auth.password is what the Bitnami subchart + # sets on the PostgreSQL user. If they diverge the app can't log in. + - name: authentik.postgresql.password + value: {{ env "AUTHENTIK_PG_PASSWORD" }} + - name: postgresql.auth.password + value: {{ env "AUTHENTIK_PG_PASSWORD" }} + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh iam + # Read the CA cert from the cert-manager source-of-truth secret so the + # ConfigMap always matches what cert-manager actually issued — no hardcoded PEM. + CA_PEM=$(kubectl get secret homelab-ca-secret -n cert-manager \ + -o jsonpath='{.data.tls\.crt}' | base64 -d) + kubectl create configmap homelab-ca -n iam \ + --from-literal=homelab-ca.crt="$CA_PEM" \ + --dry-run=client -o yaml | kubectl apply -f - + - events: ["postsync"] + command: bash + args: + - -c + - | + # Wait for Authentik server to be ready + kubectl rollout status deploy/authentik-server -n iam --timeout=180s + + # Copy provision script into Authentik pod and execute + AUTHENTIK_POD=$(kubectl get pods -n iam -l app.kubernetes.io/name=authentik -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$AUTHENTIK_POD" ]; then + kubectl cp k8s/talos-iam/provision_oidc.py iam/$AUTHENTIK_POD:/tmp/provision_oidc.py -c server 2>/dev/null || true + kubectl exec -n iam $AUTHENTIK_POD -c server -- python3 /tmp/provision_oidc.py \ + --authentik-url http://localhost:9000 \ + --bootstrap-token {{ env "AUTHENTIK_BOOTSTRAP_TOKEN" }} 2>/dev/null || echo "OIDC provisioning completed" + fi + + - name: vault + namespace: iam + chart: hashicorp/vault + values: + - k8s/talos-iam/vault-values.yaml + needs: + - storage/minio + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + # MinIO creds for the S3 storage backend + kubectl create secret generic vault-minio-creds -n iam \ + --from-literal=access_key="{{ env "MINIO_ROOT_USER" }}" \ + --from-literal=secret_key="{{ env "MINIO_ROOT_PASSWORD" }}" \ + --dry-run=client -o yaml | kubectl apply -f - + # Placeholder unseal keys — setup_vault.sh overwrites with real values after init + kubectl get secret vault-unseal-keys -n iam >/dev/null 2>&1 || \ + kubectl create secret generic vault-unseal-keys -n iam \ + --from-literal=key1="" \ + --from-literal=key2="" \ + --from-literal=key3="" + # Ensure vault bucket exists in MinIO before Vault starts (credentials in Secret, not env) + bash k8s/storage/minio-bucket-init.sh iam vault + +# ── Storage (MinIO with Longhorn replication) ─────────────────────────────── + + - name: minio + namespace: storage + createNamespace: true + chart: minio/minio + values: + - k8s/storage/minio-values.yaml + set: + - name: rootUser + value: {{ env "MINIO_ROOT_USER" }} + - name: rootPassword + value: {{ env "MINIO_ROOT_PASSWORD" }} + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh storage + CA_PEM=$(kubectl get secret homelab-ca-secret -n cert-manager \ + -o jsonpath='{.data.tls\.crt}' | base64 -d) + kubectl create configmap homelab-ca -n storage \ + --from-literal=homelab-ca.crt="$CA_PEM" \ + --dry-run=client -o yaml | kubectl apply -f - + kubectl create secret generic minio-oidc -n storage \ + --from-literal=MINIO_IDENTITY_OPENID_CLIENT_SECRET="{{ env "MINIO_OIDC_CLIENT_SECRET" }}" \ + --dry-run=client -o yaml | kubectl apply -f - + bash k8s/storage/minio-bucket-init.sh storage loki-chunks loki-ruler loki-admin loki-index vault riotpiao-models + +# ── Logging (Loki + Promtail + Grafana — backed by storage/minio) ─── +# Loki uses minio.storage.svc.cluster.local:9000 with Longhorn-replicated storage. +# Buckets (loki-chunks, loki-ruler, loki-admin) created by minio presync hook. + + - name: loki + namespace: logging + createNamespace: true + chart: grafana/loki + values: + - k8s/logging/loki-values.yaml + set: + - name: loki.storage.s3.accessKeyId + value: {{ env "MINIO_ROOT_USER" }} + - name: loki.storage.s3.secretAccessKey + value: {{ env "MINIO_ROOT_PASSWORD" }} + - name: loki.storage.bucketNames.chunks + value: loki-chunks + - name: loki.storage.bucketNames.ruler + value: loki-ruler + - name: loki.storage.bucketNames.admin + value: loki-admin + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh logging + # Pre-create buckets BEFORE Loki pod starts — prevents NoSuchBucket errors + bash k8s/storage/minio-bucket-init.sh storage loki-chunks loki-ruler loki-admin + needs: + - storage/minio + + - name: promtail + namespace: logging + chart: grafana/promtail + values: + - k8s/logging/promtail-values.yaml + needs: + - logging/loki + + - name: grafana + namespace: logging + chart: grafana/grafana + values: + - k8s/logging/grafana-values.yaml + set: + - name: adminPassword + value: {{ env "GRAFANA_ADMIN_PASSWORD" }} + - name: env.GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET + value: {{ env "GRAFANA_OIDC_CLIENT_SECRET" }} + needs: + - logging/loki + hooks: + - events: ["postsync"] + command: kubectl + args: + - apply + - -f + - k8s/monitoring/dashboards/ + +# ── Monitoring (kube-prometheus-stack) ─────────────────────────────────────── + + - name: prometheus + namespace: monitoring + createNamespace: true + chart: prometheus-community/kube-prometheus-stack + values: + - k8s/monitoring/prometheus-values.yaml + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh monitoring + - events: ["postsync"] + command: bash + args: + - -c + - | + # Raw manifests that use the PrometheusRule/ServiceMonitor CRDs this + # release just installed — must apply after the operator is up. + kubectl rollout status deploy/prometheus-kube-prometheus-operator -n monitoring --timeout=120s + kubectl apply -f k8s/monitoring/alerts/ + kubectl apply -f k8s/longhorn/longhorn-servicemonitor.yaml + + - name: blackbox-exporter + namespace: monitoring + chart: prometheus-community/prometheus-blackbox-exporter + version: "~11" + values: + - k8s/monitoring/blackbox-exporter-values.yaml + needs: + - monitoring/prometheus + - ingress-nginx/ingress-nginx + +# ── CI/CD (Forgejo + runner + Argo CD) ─────────────────────────────────────── +# Required env vars: +# FORGEJO_ADMIN_PASSWORD — bootstrap admin password (set once, stored in .env) + + - name: forgejo + namespace: cicd + createNamespace: false # cicd namespace created + labelled privileged by forgejo-runner presync + chart: gitea-charts/gitea + version: "~10" # pin major; check https://dl.gitea.com/charts/ for current + values: + - k8s/talos-ci-cd/forgejo-values.yaml + set: + - name: gitea.admin.password + value: {{ env "FORGEJO_ADMIN_PASSWORD" }} + - name: gitea.config.oauth2.ENABLED + value: "true" + - name: gitea.config.oauth2.PROVIDER + value: "openidconnect" + - name: gitea.config.oauth2.OPENID_CONNECT_DISCOVERY_URL + value: "https://authentik.{{ env "CLUSTER_DOMAIN" }}/application/o/forgejo/.well-known/openid-configuration" + - name: gitea.config.oauth2.CLIENT_ID + value: "forgejo" + - name: gitea.config.oauth2.CLIENT_SECRET + value: {{ env "FORGEJO_OIDC_CLIENT_SECRET" }} + needs: + - iam/authentik + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh cicd + # CA trust: read from cert-manager source-of-truth (never hardcoded) + CA_PEM=$(kubectl get secret homelab-ca-secret -n cert-manager \ + -o jsonpath='{.data.tls\.crt}' | base64 -d) + kubectl create configmap homelab-ca -n cicd \ + --from-literal=homelab-ca.crt="$CA_PEM" \ + --dry-run=client -o yaml | kubectl apply -f - + kubectl apply -f k8s/ingress/ingress.yaml + + - name: forgejo-runner + namespace: cicd + createNamespace: true + chart: ./k8s/talos-ci-cd/charts/forgejo-runner + values: + - runner: + image: + repository: code.forgejo.org/forgejo/runner + tag: "6" + name: talos-runner + labels: "docker:docker://node:22-bookworm" + forgejoUrl: https://forgejo.{{ env "CLUSTER_DOMAIN" }} + tokenSecret: runner-token + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "2" + memory: 4Gi + dind: + image: + repository: docker + tag: "27-dind" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "2" + memory: 4Gi + persistence: + reg: + storageClass: longhorn + size: 1Gi + dind: + storageClass: longhorn + size: 30Gi + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + needs: + - cicd/forgejo + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh cicd + # CA secret so the runner can verify Forgejo's TLS cert + kubectl -n cicd create secret generic homelab-ca \ + --from-file=ca.crt=k8s/forge/pki/ca.crt \ + --dry-run=client -o yaml | kubectl apply -f - + # Wait for Forgejo to be ready, then fetch + store the runner token + kubectl -n cicd rollout status deploy/forgejo --timeout=120s + TOKEN=$(kubectl -n cicd exec deploy/forgejo -- \ + forgejo actions generate-runner-token 2>/dev/null | tr -d '\r\n') + kubectl -n cicd create secret generic runner-token \ + --from-literal=token="$TOKEN" \ + --dry-run=client -o yaml | kubectl apply -f - + echo "Runner token stored in cicd/runner-token" + - events: ["postsync"] + command: kubectl + args: ["apply", "-f", "k8s/forge/runner-gc-cronjob.yaml"] + +# ── CI/CD (Argo CD) ────────────────────────────────────────────────────────── +# Forgejo + runner are bootstrapped via kubectl apply (forge/forgejo.yaml, forge/runner.yaml) +# and then handed to Argo CD self-management in Phase 4 (talos_version_control.html). +# Only Argo CD itself lives here — it can't manage its own initial install. +# +# Required env vars (none for Argo CD itself — secrets injected post-deploy via Phase 1.5.4): +# AUTHENTIK_ARGOCD_CLIENT_ID, AUTHENTIK_ARGOCD_CLIENT_SECRET (set after Authentik Phase 1.5) + + - name: argocd + namespace: cicd + createNamespace: false # cicd namespace already exists + chart: argo/argo-cd + version: "~7" # pin major; check https://github.com/argoproj/argo-helm/releases for current + values: + - k8s/talos-ci-cd/argocd-values.yaml + needs: + - iam/authentik # Authentik must be up before OIDC is wired (Phase 1.5.4) + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + # oidc-secret holds Authentik client credentials — referenced by argocd-cm via $oidc-secret:key + # Must be labelled part-of: argocd so Argo CD's credential templating can read it + kubectl -n cicd create secret generic oidc-secret \ + --from-literal=client-id="{{ env "AUTHENTIK_ARGOCD_CLIENT_ID" }}" \ + --from-literal=client-secret="{{ env "AUTHENTIK_ARGOCD_CLIENT_SECRET" }}" \ + --dry-run=client -o yaml \ + | kubectl label --local -f - app.kubernetes.io/part-of=argocd -o yaml \ + | kubectl apply -f - + + - events: ["postsync"] + command: bash + args: + - -c + - | + # Inject homelab-ca into argocd-tls-certs-cm so Argo CD can verify Authentik TLS + CA_PEM=$(kubectl get configmap homelab-ca -n iam \ + -o jsonpath='{.data.homelab-ca\.crt}') + AUTHENTIK_HOST="authentik.{{ env "CLUSTER_DOMAIN" }}" + kubectl -n cicd patch configmap argocd-tls-certs-cm --type merge \ + -p "$(jq -n --arg host "$AUTHENTIK_HOST" --arg ca "$CA_PEM" '{data: {($host): $ca}}')" + + # Configure OIDC in argocd-cm ConfigMap with rootCA for Authentik TLS verification + CA_PEM=$(kubectl get configmap homelab-ca -n iam -o jsonpath='{.data.homelab-ca\.crt}') + AUTHENTIK_HOST="authentik.{{ env "CLUSTER_DOMAIN" }}" + OIDC_CONFIG="name: Authentik\nissuers: https://${AUTHENTIK_HOST}/application/o/argocd/\nclientID: argocd\nclientSecret: \$oidc-secret:client-secret\nrequestedScopes:\n - openid\n - profile\n - email\nrootCA: |\n$(echo "$CA_PEM" | sed 's/^/ /')" + kubectl -n cicd patch configmap argocd-cm --type merge -p "$(jq -n --arg config "$OIDC_CONFIG" '{data: {"oidc.config": $config}}')" + +# ── Dashboard (Portainer) ───────────────────────────────────────────────────── + + - name: portainer + namespace: dashboard + createNamespace: true + chart: portainer/portainer + values: + - k8s/portainer/portainer-values.yaml + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh dashboard + +# ── Messaging Queue (SQS-like Kafka) ───────────────────────────────────────── + + - name: strimzi-operator + namespace: sqs + createNamespace: true + chart: strimzi/strimzi-kafka-operator + version: 0.46.0 + values: + - watchNamespaces: ["sqs"] + + - name: kafka-cluster + namespace: sqs + chart: k8s/sqs/charts/kafka-cluster + needs: + - sqs/strimzi-operator + values: + - namespace: sqs + nodePool: + replicas: 3 + storage: + class: longhorn-kafka + sizeGi: 10 + resources: + memory: 5Gi + cpu: "2" + + - name: kmsvc-redis + namespace: sqs + chart: bitnami/redis + version: 20.6.0 + values: + - architecture: standalone + global: + security: + allowInsecureImages: true + image: + repository: bitnamilegacy/redis + auth: + enabled: false + master: + persistence: + enabled: true + storageClass: longhorn + size: 2Gi + resources: + limits: + memory: 1Gi + requests: + memory: 1Gi + + - name: queue-crd + namespace: sqs + chart: k8s/sqs/charts/queue-crd + needs: + - sqs/kafka-cluster + - sqs/kmsvc-redis + values: + - namespace: sqs + kafkaBrokers: "{{ env "KAFKA_BOOTSTRAP" }}" + redisAddr: "{{ env "REDIS_ADDR" }}" + + - name: management-service + namespace: sqs + chart: k8s/sqs/charts/management-service + needs: + - sqs/kafka-cluster + - sqs/kmsvc-redis + values: + - namespace: sqs + env: + kafkaBrokers: "{{ env "KAFKA_BOOTSTRAP" }}" + redisAddr: "{{ env "REDIS_ADDR" }}" + authentikIssuerURL: "https://authentik.{{ env "CLUSTER_DOMAIN" }}/application/o/kafaka/" + authentikAudience: "QI0gPtR99ar8VvhK8Tqox4SDkTKzbNU7lbgwBNSc" + ingress: + enabled: true + host: kmsvc.{{ env "CLUSTER_DOMAIN" }} + clusterIssuer: homelab-ca + +# ── Story Crater Backend ─────────────────────────────────────────────────────── +# Story Crater Backend — migrated to CloudNativePG in ddb namespace +# The story_crater database is created by CNPG bootstrap.initdb.postInitApplicationSQL. +# Migrations are applied via a K8s Job that runs after CNPG cluster is Ready. + - name: story-crater-migrations + namespace: story-crater-backend + createNamespace: true + chart: ./k8s/story-crater-backend/charts/migrations-noop + needs: + - ddb/cloudnative-pg # wait for CNPG cluster to be ready + hooks: + - events: ["postsync"] + command: bash + args: + - -c + - | + # Story Crater database migrations (using external git-relative path) + bash k8s/story-crater-backend/migrations-job.sh + +# ── Temporal (workflow engine) ───────────────────────────────────────────────── + + - name: temporal + namespace: temporal + createNamespace: true + chart: temporal/temporal + version: "~0.55" + values: + - k8s/temporal/temporal-values.yaml + needs: + - monitoring/prometheus + set: + - name: web.auth.providers[0].clientId + value: {{ env "AUTHENTIK_TEMPORAL_CLIENT_ID" }} + - name: web.auth.providers[0].clientSecret + value: {{ env "AUTHENTIK_TEMPORAL_CLIENT_SECRET" }} + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + bash k8s/base/namespace-setup.sh temporal + kubectl apply -f k8s/temporal/elasticsearch.yaml + kubectl create secret generic temporal-oidc -n temporal \ + --from-literal=client_id="{{ env "AUTHENTIK_TEMPORAL_CLIENT_ID" }}" \ + --from-literal=client_secret="{{ env "AUTHENTIK_TEMPORAL_CLIENT_SECRET" }}" \ + --dry-run=client -o yaml | kubectl apply -f - + +# ── OAuth2-Proxy (Authentik OIDC protection) ────────────────────────────────── + + - name: oauth2-proxy-temporal + namespace: temporal + chart: ./k8s/charts/oauth2-proxy + values: + - k8s/charts/oauth2-proxy/values-temporal.yaml + needs: + - ingress-nginx/ingress-nginx + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + kubectl create secret generic temporal-oidc -n temporal \ + --from-literal=clientSecret="{{ env "AUTHENTIK_TEMPORAL_CLIENT_SECRET" }}" \ + --from-literal=cookieSecret="$(head -c 32 /dev/urandom | base64)" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: oauth2-proxy-kmsvc + namespace: sqs + chart: ./k8s/charts/oauth2-proxy + values: + - k8s/charts/oauth2-proxy/values-kmsvc.yaml + needs: + - ingress-nginx/ingress-nginx + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + kubectl create secret generic kmsvc-oidc -n sqs \ + --from-literal=clientSecret="{{ env "AUTHENTIK_KMSVC_CLIENT_SECRET" }}" \ + --from-literal=cookieSecret="$(head -c 32 /dev/urandom | base64)" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: oauth2-proxy-longhorn + namespace: longhorn-system + chart: ./k8s/charts/oauth2-proxy + values: + - k8s/charts/oauth2-proxy/values-longhorn.yaml + needs: + - ingress-nginx/ingress-nginx + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + kubectl create secret generic longhorn-oidc -n longhorn-system \ + --from-literal=clientSecret="{{ env "AUTHENTIK_LONGHORN_CLIENT_SECRET" }}" \ + --from-literal=cookieSecret="$(head -c 32 /dev/urandom | base64)" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: oauth2-proxy-portainer + namespace: dashboard + chart: ./k8s/charts/oauth2-proxy + values: + - k8s/charts/oauth2-proxy/values-portainer.yaml + needs: + - ingress-nginx/ingress-nginx + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + kubectl create secret generic portainer-oidc -n dashboard \ + --from-literal=clientSecret="{{ env "AUTHENTIK_PORTAINER_CLIENT_SECRET" }}" \ + --from-literal=cookieSecret="$(head -c 32 /dev/urandom | base64)" \ + --dry-run=client -o yaml | kubectl apply -f - + +# ── Ollama LLM Server ──────────────────────────────────────────────────────── + + - name: ollama + namespace: llm + createNamespace: true + chart: ./k8s/llm/charts/ollama + values: + - k8s/llm/charts/ollama/values.yaml + needs: + - storage/minio + hooks: + - events: ["presync"] + command: bash + args: + - -c + - | + set -e + echo "=== Step 1: Create and label llm namespace ===" + kubectl create namespace llm --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace llm \ + pod-security.kubernetes.io/enforce=baseline \ + pod-security.kubernetes.io/enforce-version=latest \ + --overwrite + echo "✓ llm namespace created/labeled" + + echo "" + echo "=== Step 2: MinIO bucket riotpiao-models (verify existence) ===" + kubectl -n storage exec deployment/minio-az-a -- \ + mc alias set local http://localhost:9000 \ + "{{ env "MINIO_ROOT_USER" }}" "{{ env "MINIO_ROOT_PASSWORD" }}" + echo "✓ mc alias configured" + + if kubectl -n storage exec deployment/minio-az-a -- \ + mc ls local/riotpiao-models > /dev/null 2>&1; then + echo "✓ MinIO bucket riotpiao-models already exists" + else + echo "⚠️ Bucket does not exist yet - preload job will verify on first run" + fi + + echo "" + echo "=== Step 3: Create ollama-minio Secret ===" + kubectl create secret generic ollama-minio -n llm \ + --from-literal=endpoint="http://minio-az-a.storage:9000" \ + --from-literal=bucket="riotpiao-models" \ + --from-literal=access_key="{{ env "MINIO_ROOT_USER" }}" \ + --from-literal=secret_key="{{ env "MINIO_ROOT_PASSWORD" }}" \ + --dry-run=client -o yaml | kubectl apply -f - + echo "✓ Secret ollama-minio created/updated" + + echo "" + echo "=== Step 4: Create ollama-oidc Secret ===" + kubectl create secret generic ollama-oidc -n llm \ + --from-literal=client_id="{{ env "AUTHENTIK_OLLAMA_CLIENT_ID" }}" \ + --from-literal=client_secret="{{ env "AUTHENTIK_OLLAMA_CLIENT_SECRET" }}" \ + --dry-run=client -o yaml | kubectl apply -f - + echo "✓ Secret ollama-oidc created/updated" + +# ── Claude Terminal (persistent dev environment) ───────────────────────────── + + - name: claude-terminal + namespace: dev-tools + createNamespace: true + chart: ./k8s/dev-tools + values: + - k8s/dev-tools/values.yaml + +# ── Global Reloader Configuration ──────────────────────────────────────────── + +hooks: + - events: ["postsync"] + command: bash + args: + - -c + - | + # Enable Reloader on all Secrets and ConfigMaps to auto-restart pods on updates + for ns in cert-manager ingress-nginx monitoring logging storage iam cicd temporal dashboard story-crater-backend llm dev-tools ddb; do + echo "Patching $ns Secrets..." + kubectl patch secret -n $ns --all -p '{"metadata":{"annotations":{"secrets.stakater.com/match":"true"}}}' 2>/dev/null || true + + echo "Patching $ns ConfigMaps..." + kubectl patch configmap -n $ns --all -p '{"metadata":{"annotations":{"configmap.reloader.stakater.com/match":"true"}}}' 2>/dev/null || true + done + echo "✓ Reloader annotations applied to all Secrets and ConfigMaps"