From af4f6c07ae98a57e045f9f38d1c17c02449ef430 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Sun, 16 Aug 2026 00:08:31 -0500 Subject: [PATCH] T-0.1 Flux GitOps layout + dev/prod overlays + platform component skeletons (R-GITOPS-01/03, R-ENV-02, R-PLAT-01) Co-Authored-By: Claude Opus 4.8 (1M context) --- clusters/README.md | 79 ++++++++++++++++- clusters/base/apps.yaml | 26 ++++++ clusters/base/cluster-vars.yaml | 41 +++++++++ clusters/base/flux-system.yaml | 25 ++++++ clusters/base/kustomization.yaml | 10 +++ clusters/base/platform.yaml | 32 +++++++ clusters/dev/kustomization.yaml | 46 ++++++++++ clusters/prod/kustomization.yaml | 47 ++++++++++ platform/README.md | 88 ++++++++++++++++++- platform/cert-manager/README.md | 34 +++++++ platform/cert-manager/clusterissuers.yaml | 48 ++++++++++ platform/cert-manager/helmrelease.yaml | 28 ++++++ platform/cert-manager/helmrepository.yaml | 9 ++ platform/cert-manager/kustomization.yaml | 8 ++ platform/cert-manager/namespace.yaml | 5 ++ platform/cert-manager/values.yaml | 6 ++ platform/cilium/README.md | 30 +++++++ platform/cilium/helmrelease.yaml | 55 ++++++++++++ platform/cilium/helmrepository.yaml | 9 ++ platform/cilium/kustomization.yaml | 8 ++ platform/cilium/namespace.yaml | 6 ++ platform/cilium/values.yaml | 13 +++ platform/externaldns/README.md | 32 +++++++ platform/externaldns/helmrelease.yaml | 45 ++++++++++ platform/externaldns/helmrepository.yaml | 9 ++ platform/externaldns/kustomization.yaml | 7 ++ platform/externaldns/namespace.yaml | 5 ++ platform/externaldns/values.yaml | 8 ++ platform/gateway/README.md | 34 +++++++ platform/gateway/gateway.yaml | 42 +++++++++ platform/gateway/gatewayclass.yaml | 9 ++ platform/gateway/kustomization.yaml | 8 ++ platform/gateway/namespace.yaml | 7 ++ platform/gateway/wildcard-certificate.yaml | 18 ++++ platform/image-automation/README.md | 29 ++++++ .../image-update-automation.yaml | 39 ++++++++ platform/image-automation/kustomization.yaml | 5 ++ platform/keycloak/README.md | 30 +++++++ platform/keycloak/helmrelease.yaml | 40 +++++++++ platform/keycloak/helmrepository.yaml | 11 +++ platform/keycloak/kustomization.yaml | 7 ++ platform/keycloak/namespace.yaml | 5 ++ platform/keycloak/values.yaml | 8 ++ platform/kubevirt/README.md | 31 +++++++ platform/kubevirt/cdi-cr.yaml | 11 +++ platform/kubevirt/kubevirt-cr.yaml | 16 ++++ platform/kubevirt/kustomization.yaml | 13 +++ platform/kubevirt/namespaces.yaml | 10 +++ platform/kustomization.yaml | 25 ++++++ platform/longhorn/README.md | 31 +++++++ platform/longhorn/helmrelease.yaml | 36 ++++++++ platform/longhorn/helmrepository.yaml | 9 ++ platform/longhorn/kustomization.yaml | 8 ++ platform/longhorn/namespace.yaml | 5 ++ platform/longhorn/storageclasses.yaml | 34 +++++++ platform/longhorn/values.yaml | 7 ++ platform/monitoring/README.md | 31 +++++++ platform/monitoring/alloy.yaml | 25 ++++++ platform/monitoring/helmrepositories.yaml | 18 ++++ .../monitoring/kube-prometheus-stack.yaml | 44 ++++++++++ platform/monitoring/kustomization.yaml | 9 ++ platform/monitoring/loki.yaml | 33 +++++++ platform/monitoring/namespace.yaml | 5 ++ platform/oauth2-proxy/README.md | 28 ++++++ platform/oauth2-proxy/helmrelease.yaml | 38 ++++++++ platform/oauth2-proxy/helmrepository.yaml | 9 ++ platform/oauth2-proxy/kustomization.yaml | 7 ++ platform/oauth2-proxy/namespace.yaml | 5 ++ platform/oauth2-proxy/values.yaml | 7 ++ platform/velero/README.md | 22 +++++ platform/velero/helmrelease.yaml | 46 ++++++++++ platform/velero/helmrepository.yaml | 9 ++ platform/velero/kustomization.yaml | 7 ++ platform/velero/namespace.yaml | 5 ++ platform/velero/values.yaml | 7 ++ 75 files changed, 1638 insertions(+), 4 deletions(-) create mode 100644 clusters/base/apps.yaml create mode 100644 clusters/base/cluster-vars.yaml create mode 100644 clusters/base/flux-system.yaml create mode 100644 clusters/base/kustomization.yaml create mode 100644 clusters/base/platform.yaml create mode 100644 clusters/dev/kustomization.yaml create mode 100644 clusters/prod/kustomization.yaml create mode 100644 platform/cert-manager/README.md create mode 100644 platform/cert-manager/clusterissuers.yaml create mode 100644 platform/cert-manager/helmrelease.yaml create mode 100644 platform/cert-manager/helmrepository.yaml create mode 100644 platform/cert-manager/kustomization.yaml create mode 100644 platform/cert-manager/namespace.yaml create mode 100644 platform/cert-manager/values.yaml create mode 100644 platform/cilium/README.md create mode 100644 platform/cilium/helmrelease.yaml create mode 100644 platform/cilium/helmrepository.yaml create mode 100644 platform/cilium/kustomization.yaml create mode 100644 platform/cilium/namespace.yaml create mode 100644 platform/cilium/values.yaml create mode 100644 platform/externaldns/README.md create mode 100644 platform/externaldns/helmrelease.yaml create mode 100644 platform/externaldns/helmrepository.yaml create mode 100644 platform/externaldns/kustomization.yaml create mode 100644 platform/externaldns/namespace.yaml create mode 100644 platform/externaldns/values.yaml create mode 100644 platform/gateway/README.md create mode 100644 platform/gateway/gateway.yaml create mode 100644 platform/gateway/gatewayclass.yaml create mode 100644 platform/gateway/kustomization.yaml create mode 100644 platform/gateway/namespace.yaml create mode 100644 platform/gateway/wildcard-certificate.yaml create mode 100644 platform/image-automation/README.md create mode 100644 platform/image-automation/image-update-automation.yaml create mode 100644 platform/image-automation/kustomization.yaml create mode 100644 platform/keycloak/README.md create mode 100644 platform/keycloak/helmrelease.yaml create mode 100644 platform/keycloak/helmrepository.yaml create mode 100644 platform/keycloak/kustomization.yaml create mode 100644 platform/keycloak/namespace.yaml create mode 100644 platform/keycloak/values.yaml create mode 100644 platform/kubevirt/README.md create mode 100644 platform/kubevirt/cdi-cr.yaml create mode 100644 platform/kubevirt/kubevirt-cr.yaml create mode 100644 platform/kubevirt/kustomization.yaml create mode 100644 platform/kubevirt/namespaces.yaml create mode 100644 platform/kustomization.yaml create mode 100644 platform/longhorn/README.md create mode 100644 platform/longhorn/helmrelease.yaml create mode 100644 platform/longhorn/helmrepository.yaml create mode 100644 platform/longhorn/kustomization.yaml create mode 100644 platform/longhorn/namespace.yaml create mode 100644 platform/longhorn/storageclasses.yaml create mode 100644 platform/longhorn/values.yaml create mode 100644 platform/monitoring/README.md create mode 100644 platform/monitoring/alloy.yaml create mode 100644 platform/monitoring/helmrepositories.yaml create mode 100644 platform/monitoring/kube-prometheus-stack.yaml create mode 100644 platform/monitoring/kustomization.yaml create mode 100644 platform/monitoring/loki.yaml create mode 100644 platform/monitoring/namespace.yaml create mode 100644 platform/oauth2-proxy/README.md create mode 100644 platform/oauth2-proxy/helmrelease.yaml create mode 100644 platform/oauth2-proxy/helmrepository.yaml create mode 100644 platform/oauth2-proxy/kustomization.yaml create mode 100644 platform/oauth2-proxy/namespace.yaml create mode 100644 platform/oauth2-proxy/values.yaml create mode 100644 platform/velero/README.md create mode 100644 platform/velero/helmrelease.yaml create mode 100644 platform/velero/helmrepository.yaml create mode 100644 platform/velero/kustomization.yaml create mode 100644 platform/velero/namespace.yaml create mode 100644 platform/velero/values.yaml diff --git a/clusters/README.md b/clusters/README.md index 7574723..479d715 100644 --- a/clusters/README.md +++ b/clusters/README.md @@ -1,3 +1,78 @@ -# clusters +# clusters/ -_Placeholder — populated in Phase 0. See ../home-network-k3s (read-only spec)._ +Flux entrypoints and the environment-overlay model (**R-ENV-02**, **R-GITOPS-01/02/03**). + +Governing spec: [`docs/05-gitops-and-delivery.md`](../../home-network-k3s/docs/05-gitops-and-delivery.md), +[`docs/02-environments-and-provisioning.md`](../../home-network-k3s/docs/02-environments-and-provisioning.md), +[ADR-0004 (Flux)](../../home-network-k3s/decisions/adr-0004-gitops-flux.md). + +> Phase 0 scaffolding only. These are structurally-correct stubs. **No cluster exists** — nothing +> here reconciles until WS-INFRA delivers a cluster and bootstraps Flux (roadmap Phase 1). + +## Layout — base + overlay (never fork the base) + +``` +clusters/ + base/ shared Flux objects (one authoring, both environments) + flux-system.yaml GitRepository — the reconciliation source (bootstrap → Gitea) + platform.yaml Flux Kustomization → ./platform (reconciles first) + apps.yaml Flux Kustomization → ./apps (dependsOn platform) + cluster-vars.yaml ConfigMap of overlay knobs (BASE defaults) + kustomization.yaml + dev/ ACTIVE build target — patches cluster-vars with dev values only + kustomization.yaml + prod/ DESIGN-ONLY until roadmap Phase 7 — patches cluster-vars with prod values only + kustomization.yaml +``` + +## How the overlay model satisfies R-ENV-02 + +The base authors the reconciliation graph **once**. `platform.yaml` and `apps.yaml` (Flux +`Kustomization`s) both declare `postBuild.substituteFrom` the `cluster-vars` ConfigMap. Platform and +app manifests use `${TOKEN}` placeholders for every value that legitimately differs between +environments (issuer, replica counts, storage sizes, retention, node placement). + +**`clusters/dev` and `clusters/prod` differ ONLY by the values they patch into `cluster-vars`** — +they add no resources of their own and never copy/fork a base manifest. Promotion to prod +([`docs/02`](../../home-network-k3s/docs/02-environments-and-provisioning.md)) is a change to the +prod overlay's variables, nothing else. + +## How Flux is pointed at an environment + +`flux bootstrap` (run by WS-INFRA in Phase 1) targets one environment's path per cluster: + +``` +# dev cluster +flux bootstrap ... --path=clusters/dev +# prod cluster (Phase 7 only — design-only until then) +flux bootstrap ... --path=clusters/prod +``` + +Bootstrap materializes the `flux-system` `Kustomization` that renders the chosen overlay, which pulls +in `base/` and applies the env-scoped `cluster-vars`. + +## Reconciliation order + +``` +GitRepository (flux-system) + │ + ▼ +Kustomization: platform (./platform, substituteFrom cluster-vars) + │ healthy + ▼ +Kustomization: apps (./apps, dependsOn platform, substituteFrom cluster-vars) +``` + +`apps` `dependsOn` `platform` so CNI/storage/TLS/DNS/gateway exist before any workload reconciles. + +## TODOs (later phases — do not action in Phase 0) + +- **Source cutover (WS-GITOPS, Phase 6):** `base/flux-system.yaml` currently points at the bootstrap + source. After Gitea is onboarded, repoint `spec.url` to `gitea.home.keithjackson.dev` and keep an + off-cluster mirror (spec `docs/05` "Source-of-truth", `CONTRIBUTING.md`). +- **SOPS decryption (WS-SEC, Phase 1):** add `spec.decryption.provider: sops` (age secret name only, + never plaintext) to the platform/apps Kustomizations once the age key is bootstrapped + ([ADR-0007](../../home-network-k3s/decisions/adr-0007-secrets-sops-age.md)). +- **Split ordered Flux Kustomizations** if intra-platform ordering needs to be explicit (e.g. a + separate `cilium`/`longhorn` Kustomization others `dependsOn`). + diff --git a/clusters/base/apps.yaml b/clusters/base/apps.yaml new file mode 100644 index 0000000..b2683d2 --- /dev/null +++ b/clusters/base/apps.yaml @@ -0,0 +1,26 @@ +--- +# Flux Kustomization: APPLICATION workloads. Reconciles ./apps AFTER platform is healthy. +# Phase 0 STUB. apps/ is owned by WS-APPS; this entrypoint is the GitOps contract that renders it. +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: apps + namespace: flux-system +spec: + interval: 10m + retryInterval: 2m + timeout: 5m + prune: true + wait: true + # Ordering: platform (CNI, storage, TLS, DNS, gateway, identity) must be healthy first. + dependsOn: + - name: platform + sourceRef: + kind: GitRepository + name: flux-system + path: ./apps + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-vars + # TODO(WS-SEC, Phase 1): enable SOPS decryption (ADR-0007) — reference age secret by name only. diff --git a/clusters/base/cluster-vars.yaml b/clusters/base/cluster-vars.yaml new file mode 100644 index 0000000..5ea6d2e --- /dev/null +++ b/clusters/base/cluster-vars.yaml @@ -0,0 +1,41 @@ +--- +# cluster-vars — the overlay knob surface (R-ENV-02). +# +# This ConfigMap holds BASE DEFAULTS. Flux `postBuild.substituteFrom` injects these into every +# `${TOKEN}` in platform/ and apps/ manifests at reconcile time. dev/ and prod/ overlays STRATEGIC- +# MERGE PATCH this single ConfigMap — that patch is the *entire* difference between environments. +# +# Add a knob here (with a base default) before referencing `${KNOB}` in a component manifest, then +# give each overlay its env-scoped value. Never fork a base manifest to differ per environment. +apiVersion: v1 +kind: ConfigMap +metadata: + name: cluster-vars + namespace: flux-system +data: + # --- identity of the environment --- + cluster_env: "base" # dev | prod (overlays set this) + + # --- DNS / TLS --- + base_domain: "home.keithjackson.dev" # same in both envs (split-horizon per docs/09) + acme_cluster_issuer: "letsencrypt-staging" # dev: staging | prod: letsencrypt-prod (ADR-0006) + + # --- storage (ADR-0003) --- + longhorn_default_replica_count: "3" # default StorageClass replica count (3 on 3 nodes) + + # --- observability sizing (docs/10) --- + monitoring_prometheus_retention: "7d" # dev short | prod long + monitoring_prometheus_pvc_size: "10Gi" + monitoring_loki_pvc_size: "10Gi" + + # --- identity / auth (ADR-0014) --- + keycloak_replicas: "1" # dev 1 | prod 2 (HA) + oauth2_proxy_replicas: "1" # dev 1 | prod 2 (HA) + + # --- backup / DR (ADR-0003, R-STATE-03) --- + velero_backup_schedule: "0 3 * * *" # cron; overlays may differ + velero_retention: "168h" # dev short | prod long + + # --- placement --- + platform_node_selector_key: "kubernetes.io/os" # base no-op selector; prod may pin roles + platform_node_selector_value: "linux" diff --git a/clusters/base/flux-system.yaml b/clusters/base/flux-system.yaml new file mode 100644 index 0000000..38d7ba2 --- /dev/null +++ b/clusters/base/flux-system.yaml @@ -0,0 +1,25 @@ +--- +# GitRepository — the single source of truth Flux reconciles from (R-GITOPS-01). +# Phase 0 STUB: not applied until Flux is bootstrapped in Phase 1 (WS-INFRA). +# +# Bootstrapping sequence (spec docs/05 "Source-of-truth", CONTRIBUTING.md): +# 1. Initially points at the BOOTSTRAP SOURCE (this local repo / out-of-band). +# 2. Gitea is onboarded early (roadmap Phase 6). +# 3. TODO(WS-GITOPS, Phase 6): repoint spec.url -> https://gitea.home.keithjackson.dev//home-network-k3s-impl +# and keep an off-cluster mirror so a source-of-truth outlives the cluster. +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 1m + # TODO(WS-INFRA/WS-GITOPS): set to the bootstrap source URL at Phase-1 bootstrap. + url: https://REPLACE-ME.invalid/home-network-k3s-impl.git + ref: + # Reconcile the tracked branch. Overlay may patch this if dev/prod track different branches; + # the default model tracks a single branch and differentiates by clusters/ path. + branch: main + # TODO(WS-SEC, Phase 1): reference the SOPS/age or deploy-key secret BY NAME only (no plaintext). + # secretRef: + # name: flux-system # created by `flux bootstrap` diff --git a/clusters/base/kustomization.yaml b/clusters/base/kustomization.yaml new file mode 100644 index 0000000..cd5f233 --- /dev/null +++ b/clusters/base/kustomization.yaml @@ -0,0 +1,10 @@ +--- +# Base Flux entrypoint set. Shared by BOTH environment overlays (clusters/dev, clusters/prod). +# Overlays reference this base and patch ONLY the cluster-vars ConfigMap (R-ENV-02). +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - flux-system.yaml # GitRepository (reconciliation source) + - platform.yaml # Flux Kustomization -> ./platform + - apps.yaml # Flux Kustomization -> ./apps (dependsOn platform) + - cluster-vars.yaml # overlay knob surface (base defaults) diff --git a/clusters/base/platform.yaml b/clusters/base/platform.yaml new file mode 100644 index 0000000..1f3dec6 --- /dev/null +++ b/clusters/base/platform.yaml @@ -0,0 +1,32 @@ +--- +# Flux Kustomization: PLATFORM services (cluster-wide). Reconciles ./platform. +# Phase 0 STUB. Realizes R-GITOPS-01/03 (pull-based reconcile + drift correction) and +# R-ENV-02 (env differences supplied ONLY via substituteFrom cluster-vars). +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: platform + namespace: flux-system +spec: + interval: 10m + retryInterval: 2m + timeout: 5m + prune: true # drift correction: delete resources removed from git (R-GITOPS-03) + wait: true # block `apps` until platform is healthy + sourceRef: + kind: GitRepository + name: flux-system + path: ./platform + # R-ENV-02: the ONLY place environment differences enter platform manifests. + # dev/prod overlays patch the cluster-vars ConfigMap; the base manifests are never forked. + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-vars + # TODO(WS-SEC, Phase 1): enable SOPS decryption once the age key exists (ADR-0007). + # decryption: + # provider: sops + # secretRef: + # name: sops-age + # TODO(PM): the top-level ./platform/kustomization.yaml must include platform/kyverno/ + # (owned by the policy engineer, WS-TEST). See platform/README.md integration TODO. diff --git a/clusters/dev/kustomization.yaml b/clusters/dev/kustomization.yaml new file mode 100644 index 0000000..2598897 --- /dev/null +++ b/clusters/dev/kustomization.yaml @@ -0,0 +1,46 @@ +--- +# clusters/dev — the ACTIVE build target overlay. +# +# `flux bootstrap ... --path=clusters/dev` points the dev cluster's Flux here. +# This overlay adds NO resources of its own. Its ONLY job is to patch the shared cluster-vars +# ConfigMap with dev-scoped values (R-ENV-02). The base reconciliation graph is reused verbatim. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../base +patches: + - target: + kind: ConfigMap + name: cluster-vars + patch: |- + - op: replace + path: /data/cluster_env + value: "dev" + # DNS-01 against LE staging while iterating (avoids prod rate limits) — ADR-0006. + - op: replace + path: /data/acme_cluster_issuer + value: "letsencrypt-staging" + # dev mirrors prod's 3-node topology, so the replicated tier keeps 3 replicas. + - op: replace + path: /data/longhorn_default_replica_count + value: "3" + # smaller retention / volumes to keep the dev workstation footprint light. + - op: replace + path: /data/monitoring_prometheus_retention + value: "3d" + - op: replace + path: /data/monitoring_prometheus_pvc_size + value: "5Gi" + - op: replace + path: /data/monitoring_loki_pvc_size + value: "5Gi" + # single-replica control-plane addons in dev (no HA needed to iterate). + - op: replace + path: /data/keycloak_replicas + value: "1" + - op: replace + path: /data/oauth2_proxy_replicas + value: "1" + - op: replace + path: /data/velero_retention + value: "72h" diff --git a/clusters/prod/kustomization.yaml b/clusters/prod/kustomization.yaml new file mode 100644 index 0000000..78eec6b --- /dev/null +++ b/clusters/prod/kustomization.yaml @@ -0,0 +1,47 @@ +--- +# clusters/prod — DESIGN-ONLY overlay. NOT a build target until roadmap Phase 7 opens prod +# (spec roadmap/phases.md, AGENTS.md §6: "Never build directly on production"). +# +# When Phase 7 is opened, `flux bootstrap ... --path=clusters/prod` points the prod cluster's Flux +# here. Like dev, this overlay adds NO resources — it ONLY patches cluster-vars with prod-scoped +# values (R-ENV-02). Promotion to prod is a change to THESE variables, never a base edit. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../base +patches: + - target: + kind: ConfigMap + name: cluster-vars + patch: |- + - op: replace + path: /data/cluster_env + value: "prod" + # trusted, rate-limited real certs in prod — ADR-0006. + - op: replace + path: /data/acme_cluster_issuer + value: "letsencrypt-prod" + - op: replace + path: /data/longhorn_default_replica_count + value: "3" + # longer retention / larger volumes on prod NVMe. + - op: replace + path: /data/monitoring_prometheus_retention + value: "30d" + - op: replace + path: /data/monitoring_prometheus_pvc_size + value: "50Gi" + - op: replace + path: /data/monitoring_loki_pvc_size + value: "50Gi" + # HA control-plane addons in prod. + - op: replace + path: /data/keycloak_replicas + value: "2" + - op: replace + path: /data/oauth2_proxy_replicas + value: "2" + - op: replace + path: /data/velero_retention + value: "720h" + # TODO(WS-INFRA, Phase 7): pin prod node placement once the bare-metal role labels exist. diff --git a/platform/README.md b/platform/README.md index 707e1b0..2d800a9 100644 --- a/platform/README.md +++ b/platform/README.md @@ -1,3 +1,87 @@ -# platform +# platform/ -_Placeholder — populated in Phase 0. See ../home-network-k3s (read-only spec)._ +Cluster-wide **platform services**, Flux-managed (**R-PLAT-01**, **R-GITOPS-01/02/03**). Reconciled +by the `platform` Flux Kustomization defined in [`clusters/base/platform.yaml`](../clusters/base/platform.yaml), +**before** `apps/`. + +Governing spec: [`docs/05-gitops-and-delivery.md`](../../home-network-k3s/docs/05-gitops-and-delivery.md), +[`docs/01-architecture-overview.md`](../../home-network-k3s/docs/01-architecture-overview.md). + +> **Phase 0 scaffolding only.** Every component here is a structurally-correct **stub** — a Flux +> HelmRelease and/or Kustomization with a values placeholder, a README citing the governing ADR/spec, +> and TODOs for the phase that fills it in. **No cluster exists**; nothing reconciles yet. Do not add +> working config, secrets, or provisioning under Phase 0 (spec `AGENTS.md` §1/§5/§6, `roadmap/phases.md`). + +## Reconciliation flow (how a change lands) + +``` +git (source of truth) + │ operator PR → CI (lint · schema · Kyverno L0 · Chainsaw) → merge to tracked branch + ▼ +Flux GitRepository (clusters/base/flux-system.yaml) + ▼ +Flux Kustomization "platform" → ./platform (this dir) ── substituteFrom cluster-vars ──┐ + │ prune=true (drift correction, R-GITOPS-03) · wait=true │ R-ENV-02 + ▼ │ +Flux Kustomization "apps" → ./apps (dependsOn platform) ─ substituteFrom cluster-vars ──┘ +``` + +Git is authoritative; the cluster conforms to git and drift is auto-corrected (R-GITOPS-01/03). +Manual `kubectl apply` of platform state is prohibited (spec `AGENTS.md` §5). + +## The dev ↔ prod overlay model (R-ENV-02) + +Platform manifests are authored **once**. Every value that legitimately differs between environments +is a `${TOKEN}` resolved from the `cluster-vars` ConfigMap via Flux `postBuild.substituteFrom`. The +`clusters/dev` and `clusters/prod` overlays differ **only** by the values they patch into +`cluster-vars` (issuer, replica counts, storage sizes, retention). See +[`clusters/README.md`](../clusters/README.md). Base manifests are **never forked per environment**. + +## Components (one directory each) + +| Dir | Component | Governing ADR / spec | Serves | +|---|---|---|---| +| `cilium/` | CNI + kube-proxy replacement + Gateway API + Egress Gateway + Hubble | [ADR-0002](../../home-network-k3s/decisions/adr-0002-cni-cilium.md), docs/03 | R-NET-* | +| `longhorn/` | Replicated block storage; 3-replica default StorageClass + bulk tier | [ADR-0003](../../home-network-k3s/decisions/adr-0003-storage-longhorn.md), docs/04 | R-STATE-* | +| `cert-manager/` | TLS via Let's Encrypt DNS-01 (Cloudflare); staging + prod ClusterIssuers | [ADR-0006](../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md), docs/08 | R-TLS-* | +| `externaldns/` | Automatic DNS records for public services (Cloudflare) | docs/09 | R-DNS-* | +| `gateway/` | Gateway API GatewayClass + shared HTTPS Gateway + wildcard cert | [ADR-0002](../../home-network-k3s/decisions/adr-0002-cni-cilium.md)/[ADR-0006](../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md), docs/09 | R-DNS-01, R-TLS-01 | +| `monitoring/` | kube-prometheus-stack + Loki + Grafana Alloy + Alertmanager | docs/10 | R-OBS-* | +| `velero/` | Off-cluster backup/DR of volumes + resource state | [ADR-0003](../../home-network-k3s/decisions/adr-0003-storage-longhorn.md), docs/04 | R-STATE-03 | +| `kubevirt/` | KubeVirt + CDI (IaaS: application VMs on the same K8s surface) | [ADR-0005](../../home-network-k3s/decisions/adr-0005-vm-workloads-kubevirt.md), docs/06 | R-PLAT-03 | +| `image-automation/` | Flux image-update-automation (hands-off image tag advance) | [ADR-0009](../../home-network-k3s/decisions/adr-0009-automated-image-updates.md), docs/11 | R-UPD-02 | +| `keycloak/` | Platform IdP (OIDC + SAML), config-as-code, PostgreSQL on Longhorn | [ADR-0014](../../home-network-k3s/decisions/adr-0014-idp-keycloak.md), docs/15 | R-IDP-* | +| `oauth2-proxy/` | Forward-auth at the Gateway for non-OIDC apps → Keycloak | [ADR-0014](../../home-network-k3s/decisions/adr-0014-idp-keycloak.md), docs/15 | R-IDP-* | +| `kyverno/` | **L0 policy-as-code gate — OWNED BY THE POLICY ENGINEER (WS-TEST).** | [ADR-0011](../../home-network-k3s/decisions/adr-0011-test-tooling-kyverno-chainsaw.md), docs/14 | R-TEST-* | + +## ⚠️ INTEGRATION TODO(PM): wire kyverno into the platform kustomization + +`platform/kyverno/` is authored by a **different agent** (the policy engineer, WS-TEST). This +workstream did **not** create any files under `platform/kyverno/` and must not. + +[`platform/kustomization.yaml`](kustomization.yaml) intentionally **omits `- kyverno`**. Once the +policy engineer's files land, the **PM** must add `- kyverno` to that list (recommended first, so +policies are present before other components reconcile). The line is present as a commented TODO in +the kustomization. + +## Secrets + +Every secret referenced below (Cloudflare API token, Velero backup-target creds, Keycloak admin/DB, +oauth2-proxy cookie/client secrets) is referenced **by name only** and will be committed +**SOPS-encrypted** (WS-SEC, [ADR-0007](../../home-network-k3s/decisions/adr-0007-secrets-sops-age.md)). +**No plaintext secret appears anywhere in this tree.** + +## Cross-workstream contracts surfaced by these stubs + +- **cilium (WS-NET ↔ WS-INFRA):** Cilium's *initial* CNI install is done by Ansible at cluster + bootstrap (docs/02, Phase 1). The HelmRelease here manages Cilium *config* thereafter; its Helm + values MUST match the Ansible bootstrap values (kubeProxyReplacement, k8sServiceHost/Port, API + server host) or reconciliation will fight the bootstrap. Contract owner: WS-NET + WS-INFRA. +- **gateway + cert-manager + externaldns (WS-NET + WS-SEC):** the shared Gateway listener, the + wildcard `*.home.keithjackson.dev` cert, and the ExternalDNS source all reference each other; they + form the "Issuer + Gateway TLS" interface in `roadmap/workstreams.md`. +- **image-automation (WS-UPDATE):** this dir holds only the *cluster-wide* ImageUpdateAutomation + + git write-back. Per-service `ImageRepository`/`ImagePolicy` are rendered from each + ServiceDefinition's `update` block (WS-GITOPS/WS-UPDATE), living with `apps/`. +- **keycloak/oauth2-proxy (WS-SEC):** the SSO/auth contract (`auth` block → Keycloak client / + oauth2-proxy protection) is owned by WS-SEC; these are placeholders pending that contract. diff --git a/platform/cert-manager/README.md b/platform/cert-manager/README.md new file mode 100644 index 0000000..35de030 --- /dev/null +++ b/platform/cert-manager/README.md @@ -0,0 +1,34 @@ +# platform/cert-manager + +**cert-manager** — trusted TLS via **Let's Encrypt**, **ACME DNS-01** (Cloudflare). + +- **Governing ADR:** [ADR-0006 — TLS: cert-manager + Let's Encrypt (DNS-01)](../../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md) +- **Spec:** [docs/08 — TLS & Certificates](../../../home-network-k3s/docs/08-tls-and-certificates.md), + [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md) +- **Serves:** R-TLS-01/02/03, R-DNS-01 +- **Owner:** WS-SEC + +## What this stub is + +Flux `HelmRelease` + **two `ClusterIssuer`s** (`letsencrypt-staging`, `letsencrypt-prod` — both +mandated by ADR-0006). DNS-01 against the `keithjackson.dev` zone validates domain control **without +exposing the service** (works for internal-only services, R-TLS-03) and enables the **wildcard** +`*.home.keithjackson.dev` cert defined in [`../gateway`](../gateway). + +## Contract (Issuer + Gateway TLS) + +Part of the "Issuer + Gateway TLS" interface (`roadmap/workstreams.md`, WS-SEC + WS-NET): the wildcard +`Certificate` (in `gateway/`) references the issuer named by `${ACME_CLUSTER_ISSUER}` and its secret +is consumed by the Gateway HTTPS listener. + +## Secrets + +The Cloudflare API token is referenced **by name** (`cloudflare-api-token`) and committed +**SOPS-encrypted** (WS-SEC, OQ-4, ADR-0007). **No plaintext token in git.** + +## TODO (later phases) + +- **OQ-4 (resolved to Cloudflare):** WS-SEC provisions the scoped Cloudflare token, SOPS-encrypted. +- Set the ACME account email. +- **Phase 2 (WS-SEC):** issue the wildcard cert; prove a trusted, non-self-signed cert at + `*.home.keithjackson.dev` (R-TLS-01/03, Phase-2 exit gate). diff --git a/platform/cert-manager/clusterissuers.yaml b/platform/cert-manager/clusterissuers.yaml new file mode 100644 index 0000000..6d4369d --- /dev/null +++ b/platform/cert-manager/clusterissuers.yaml @@ -0,0 +1,48 @@ +--- +# ClusterIssuers — Let's Encrypt via ACME DNS-01 against the keithjackson.dev zone (Cloudflare). +# ADR-0006 mandates BOTH a staging issuer (bring-up) and a prod issuer. DNS-01 enables wildcard +# certs and does NOT require exposing the service (works for internal-only services, R-TLS-03). +# +# Phase 0 STUB: emails/tokens are placeholders. The Cloudflare API token is referenced BY NAME only +# and will be committed SOPS-encrypted (OQ-4, ADR-0007) — NO plaintext secret here. +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: REPLACE-ME@keithjackson.dev # TODO(WS-SEC): ACME account email + privateKeySecretRef: + name: letsencrypt-staging-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token # SOPS-encrypted (WS-SEC) — name only + key: api-token + selector: + dnsZones: + - "keithjackson.dev" +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: REPLACE-ME@keithjackson.dev # TODO(WS-SEC): ACME account email + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token # SOPS-encrypted (WS-SEC) — name only + key: api-token + selector: + dnsZones: + - "keithjackson.dev" +# The active issuer per environment is chosen via ${ACME_CLUSTER_ISSUER} (cluster-vars): +# dev -> letsencrypt-staging, prod -> letsencrypt-prod (R-ENV-02). See gateway/ wildcard Certificate. diff --git a/platform/cert-manager/helmrelease.yaml b/platform/cert-manager/helmrelease.yaml new file mode 100644 index 0000000..aa48ba5 --- /dev/null +++ b/platform/cert-manager/helmrelease.yaml @@ -0,0 +1,28 @@ +--- +# cert-manager — issues trusted Let's Encrypt certs via ACME DNS-01 (ADR-0006). Phase 0 STUB. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cert-manager + namespace: cert-manager +spec: + interval: 30m + chart: + spec: + chart: cert-manager + version: ">=1.16.0 <1.17.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: jetstack + namespace: cert-manager + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + crds: + enabled: true # install CRDs with the chart + # TODO(WS-SEC, Phase 2): DNS-01 recursive nameserver config if split-horizon DNS interferes + # with propagation checks (docs/09 split-horizon note). diff --git a/platform/cert-manager/helmrepository.yaml b/platform/cert-manager/helmrepository.yaml new file mode 100644 index 0000000..211f93f --- /dev/null +++ b/platform/cert-manager/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: jetstack + namespace: cert-manager +spec: + interval: 1h + url: https://charts.jetstack.io diff --git a/platform/cert-manager/kustomization.yaml b/platform/cert-manager/kustomization.yaml new file mode 100644 index 0000000..59b86b8 --- /dev/null +++ b/platform/cert-manager/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml + - clusterissuers.yaml diff --git a/platform/cert-manager/namespace.yaml b/platform/cert-manager/namespace.yaml new file mode 100644 index 0000000..6bc19f4 --- /dev/null +++ b/platform/cert-manager/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager diff --git a/platform/cert-manager/values.yaml b/platform/cert-manager/values.yaml new file mode 100644 index 0000000..ddc0d08 --- /dev/null +++ b/platform/cert-manager/values.yaml @@ -0,0 +1,6 @@ +# cert-manager Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# crds.enabled: true # install CRDs with the chart +# dns01RecursiveNameservers / -Only # TODO(WS-SEC): may be needed with split-horizon DNS +# ClusterIssuers (letsencrypt-staging/-prod) live in clusterissuers.yaml, not here. +# Cloudflare API token: SOPS-encrypted secret `cloudflare-api-token` (WS-SEC, OQ-4) — name only. diff --git a/platform/cilium/README.md b/platform/cilium/README.md new file mode 100644 index 0000000..cee130e --- /dev/null +++ b/platform/cilium/README.md @@ -0,0 +1,30 @@ +# platform/cilium + +**Cilium** — CNI, kube-proxy replacement, Gateway API ingress, Egress Gateway, and Hubble. + +- **Governing ADR:** [ADR-0002 — CNI, network policy & ingress: Cilium + Gateway API](../../../home-network-k3s/decisions/adr-0002-cni-cilium.md) +- **Spec:** [docs/03 — Networking](../../../home-network-k3s/docs/03-networking-isolation-and-egress.md), + [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md) +- **Serves:** R-NET-01..07, R-DNS-01, R-SEC-02 +- **Owner:** WS-NET (bootstrap shared with WS-INFRA) + +## What this stub is + +A Flux `HelmRelease` + `HelmRepository` stub with placeholder values. **No cluster exists** (Phase 0); +this does not reconcile. Cilium replaces k3s' bundled **flannel + kube-proxy + Traefik** +(ADR-0001/0002). + +## Bootstrap vs. GitOps (important) + +Per [docs/02](../../../home-network-k3s/docs/02-environments-and-provisioning.md), Cilium's **initial** +install is done by the **Ansible k3s bootstrap** (Phase 1, WS-INFRA), because a cluster needs a CNI +before Flux can run. This HelmRelease **manages Cilium configuration thereafter**. The Helm values +here MUST stay consistent with the Ansible bootstrap values (`kubeProxyReplacement`, `k8sServiceHost`, +`k8sServicePort`) — otherwise Flux and the bootstrap fight. **Coordinate this contract with WS-INFRA.** + +## TODO (later phases — not Phase 0) + +- **Phase 1 (WS-INFRA/WS-NET):** align Helm values with the Ansible bootstrap; pin chart version. +- **Phase 2 (WS-NET):** default-deny network baseline; Egress Gateway policies; L2 announcement IP + pool + `CiliumL2AnnouncementPolicy` for the ingress VIP; Hubble metrics → kube-prometheus-stack. +- **Phase 3 (WS-NET):** egress-class routing integration with the VPN gateway (ADR-0013). diff --git a/platform/cilium/helmrelease.yaml b/platform/cilium/helmrelease.yaml new file mode 100644 index 0000000..1d6fdba --- /dev/null +++ b/platform/cilium/helmrelease.yaml @@ -0,0 +1,55 @@ +--- +# Cilium — CNI, kube-proxy replacement, Gateway API, Egress Gateway, Hubble (ADR-0002). +# Phase 0 STUB. Values are PLACEHOLDERS; fill in during Phase 1/2 (WS-NET). +# +# IMPORTANT (cross-workstream contract): Cilium's INITIAL install is performed by the Ansible +# bootstrap (spec docs/02, Phase 1), replacing k3s flannel/kube-proxy. This HelmRelease manages +# Cilium CONFIG going forward. Its values MUST match the Ansible bootstrap values (kubeProxyReplacement, +# k8sServiceHost/Port) or Flux and the bootstrap will fight. Owner: WS-NET + WS-INFRA. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cilium + namespace: kube-system +spec: + interval: 30m + chart: + spec: + chart: cilium + version: ">=1.16.0 <1.17.0" # TODO(WS-NET): pin exact version at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: cilium + namespace: kube-system + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + # Placeholder values — see values.yaml for the annotated skeleton. Inlined here so the intent + # (Cilium REPLACES flannel + kube-proxy + Traefik) is visible at the release. + values: + # --- kube-proxy replacement (ADR-0001/0002: k3s kube-proxy disabled) --- + kubeProxyReplacement: true + # TODO(WS-INFRA): k8sServiceHost/k8sServicePort must equal the Ansible bootstrap's API endpoint. + k8sServiceHost: "REPLACE-ME" # e.g. the k3s API VIP/host + k8sServicePort: 6443 + # --- Gateway API (ingress; replaces Traefik) — docs/09, ADR-0002 --- + gatewayAPI: + enabled: true + # --- Egress Gateway (egress-class steering; part of VPN model with ADR-0013) --- + egressGateway: + enabled: true + # --- Hubble (flow observability / isolation audit) --- + hubble: + enabled: true + relay: + enabled: true + ui: + enabled: true # exposure gated behind oauth2-proxy forward-auth later (ADR-0014) + # --- L2 announcement for the ingress VIP on the flat LAN (docs/09, OQ-1 resolved) --- + l2announcements: + enabled: true + # TODO(WS-NET, Phase 2): default-deny baseline, CiliumL2AnnouncementPolicy + IP pool, + # Egress Gateway policies, Hubble metrics export to kube-prometheus-stack. diff --git a/platform/cilium/helmrepository.yaml b/platform/cilium/helmrepository.yaml new file mode 100644 index 0000000..629ce00 --- /dev/null +++ b/platform/cilium/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: cilium + namespace: kube-system +spec: + interval: 1h + url: https://helm.cilium.io/ diff --git a/platform/cilium/kustomization.yaml b/platform/cilium/kustomization.yaml new file mode 100644 index 0000000..3c37953 --- /dev/null +++ b/platform/cilium/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kube-system +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml diff --git a/platform/cilium/namespace.yaml b/platform/cilium/namespace.yaml new file mode 100644 index 0000000..f3c1df7 --- /dev/null +++ b/platform/cilium/namespace.yaml @@ -0,0 +1,6 @@ +--- +# Cilium runs in kube-system (it is the CNI). Declared for completeness; k3s provides it. +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system diff --git a/platform/cilium/values.yaml b/platform/cilium/values.yaml new file mode 100644 index 0000000..c884d23 --- /dev/null +++ b/platform/cilium/values.yaml @@ -0,0 +1,13 @@ +# Cilium Helm values — PLACEHOLDER skeleton (Phase 0). Authoritative values live inline in +# helmrelease.yaml for now; this file documents the knobs WS-NET must set in Phase 1/2. +# +# MUST match the Ansible bootstrap (docs/02) for the fields the bootstrap also sets. +# +# kubeProxyReplacement: true # k3s kube-proxy disabled (ADR-0001/0002) +# k8sServiceHost / k8sServicePort # = Ansible bootstrap API endpoint (WS-INFRA contract) +# gatewayAPI.enabled: true # ingress via Gateway API, no Traefik (docs/09) +# egressGateway.enabled: true # egress-class steering (ADR-0002, VPN model ADR-0013) +# hubble.{enabled,relay,ui} # flow observability + isolation audit +# l2announcements.enabled: true # ingress VIP on flat LAN (docs/09, OQ-1) +# ipam / ipv4NativeRoutingCIDR # TODO: pin to the cluster pod/LAN CIDRs at bring-up +# encryption (wireguard/ipsec) # TODO(WS-SEC/WS-NET): decide east-west encryption later diff --git a/platform/externaldns/README.md b/platform/externaldns/README.md new file mode 100644 index 0000000..005b1ec --- /dev/null +++ b/platform/externaldns/README.md @@ -0,0 +1,32 @@ +# platform/externaldns + +**ExternalDNS** — automatic public DNS records for `public` services (Cloudflare). + +- **Spec:** [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md) + (ExternalDNS is named in [docs/01](../../../home-network-k3s/docs/01-architecture-overview.md); no + dedicated ADR) +- **Serves:** R-DNS-01/02/03 +- **Owner:** WS-NET (token from WS-SEC) + +## What this stub is + +Flux `HelmRelease` watching Gateway/HTTPRoute + Service resources, publishing +`.home.keithjackson.dev` records in Cloudflare from declared intent (no manual record +editing, R-DNS-02). + +## Split-horizon note (docs/09) + +A wildcard `*.keithjackson.dev` matches only one label deep, so `.home.keithjackson.dev` needs +its own record. **`internal` services need NO public record** — PiHole resolves them to the internal +Gateway VIP (split-horizon). ExternalDNS manages **`public`** services' records only. + +## Secrets + +Cloudflare API token referenced **by name** (`cloudflare-api-token`, shared with cert-manager), +**SOPS-encrypted** (WS-SEC, OQ-4). + +## TODO (later phases) + +- **Phase 2 (WS-NET):** confirm `gateway-httproute` source; prove an auto record for a test route + (R-DNS-02, Phase-2 exit gate). +- **Phase 6 (WS-APPS):** PiHole provides the internal split-horizon answers as it onboards. diff --git a/platform/externaldns/helmrelease.yaml b/platform/externaldns/helmrelease.yaml new file mode 100644 index 0000000..ea8fc96 --- /dev/null +++ b/platform/externaldns/helmrelease.yaml @@ -0,0 +1,45 @@ +--- +# ExternalDNS — publishes public DNS records for `public` services automatically (docs/09, R-DNS-02). +# Watches Gateway/HTTPRoute (+Service); manages records in Cloudflare. Phase 0 STUB. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: external-dns + namespace: external-dns +spec: + interval: 30m + chart: + spec: + chart: external-dns + version: ">=1.15.0 <1.16.0" # TODO(WS-NET): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: external-dns + namespace: external-dns + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + provider: + name: cloudflare + # Watch Gateway API routes (Cilium Gateway) + Services. docs/09: records rendered from intent. + sources: + - gateway-httproute + - service + # Only manage the platform zone. `internal` services need no public record (split-horizon via + # PiHole) — docs/09 "DNS record depth". + domainFilters: + - "home.keithjackson.dev" + policy: sync # create + update + delete records as services come and go + # Cloudflare API token: SOPS-encrypted secret, referenced BY NAME only (OQ-4, ADR-0007). + env: + - name: CF_API_TOKEN + valueFrom: + secretKeyRef: + name: cloudflare-api-token + key: api-token + # TODO(WS-NET, Phase 2): confirm gateway-httproute source wiring; prove a record appears + # automatically for a test route (R-DNS-02, Phase-2 exit gate). diff --git a/platform/externaldns/helmrepository.yaml b/platform/externaldns/helmrepository.yaml new file mode 100644 index 0000000..4535903 --- /dev/null +++ b/platform/externaldns/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: external-dns + namespace: external-dns +spec: + interval: 1h + url: https://kubernetes-sigs.github.io/external-dns/ diff --git a/platform/externaldns/kustomization.yaml b/platform/externaldns/kustomization.yaml new file mode 100644 index 0000000..5704992 --- /dev/null +++ b/platform/externaldns/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml diff --git a/platform/externaldns/namespace.yaml b/platform/externaldns/namespace.yaml new file mode 100644 index 0000000..3e353b5 --- /dev/null +++ b/platform/externaldns/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: external-dns diff --git a/platform/externaldns/values.yaml b/platform/externaldns/values.yaml new file mode 100644 index 0000000..6cc99da --- /dev/null +++ b/platform/externaldns/values.yaml @@ -0,0 +1,8 @@ +# ExternalDNS Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# provider.name: cloudflare +# sources: [gateway-httproute, service] # Cilium Gateway API + Services (docs/09) +# domainFilters: [home.keithjackson.dev] # only the platform zone +# policy: sync # create/update/delete records with services +# Cloudflare token: SOPS-encrypted `cloudflare-api-token` (WS-SEC, OQ-4) — name only. +# NOTE: `internal` services need NO public record (PiHole split-horizon, docs/09). diff --git a/platform/gateway/README.md b/platform/gateway/README.md new file mode 100644 index 0000000..d403257 --- /dev/null +++ b/platform/gateway/README.md @@ -0,0 +1,34 @@ +# platform/gateway + +**Gateway API** (north-south ingress) implemented by **Cilium** + the shared HTTPS Gateway and the +wildcard TLS certificate. + +- **Governing ADRs:** [ADR-0002 — Cilium + Gateway API](../../../home-network-k3s/decisions/adr-0002-cni-cilium.md), + [ADR-0006 — cert-manager/Let's Encrypt](../../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md) +- **Spec:** [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md) +- **Serves:** R-DNS-01, R-TLS-01 +- **Owner:** WS-NET (+ WS-SEC for the cert) + +## What this stub is + +- **`GatewayClass` `cilium`** — Gateway API implemented by Cilium; no separate ingress controller + (ADR-0002). +- **`Gateway` `shared`** — HTTPS listener on `*.home.keithjackson.dev`, TLS terminated with the + wildcard cert. App HTTPRoutes attach cross-namespace via a namespace label + ReferenceGrant + (rendered by WS-APPS from each ServiceDefinition's `exposure`). +- **`Certificate` `wildcard-home`** — `*.home.keithjackson.dev` issued by `${ACME_CLUSTER_ISSUER}` + (staging in dev, prod in prod — R-ENV-02). Makes onboarding certificate-free (ADR-0006 default). + +## Contract (Issuer + Gateway TLS) + +The "Issuer + Gateway TLS" interface (`roadmap/workstreams.md`): this Gateway + wildcard cert are what +every service's HTTPRoute attaches to. ExternalDNS ([`../externaldns`](../externaldns)) publishes the +`public` records for hostnames routed here. Ingress opens **north-south** only — it does **not** relax +east-west default-deny (docs/09). + +## TODO (later phases) + +- **Phase 2 (WS-NET):** pin the L2-announced ingress VIP / IP pool (Cilium L2 announcement, docs/09); + add the HTTP→HTTPS redirect route; confirm the cross-namespace ReferenceGrant pattern with WS-APPS. +- **Public exposure** is a **manual edge port-forward** of 80/443 to the Gateway VIP (single SOHO + router, docs/09) — a documented manual step, the analogue of the old pfSense exception. diff --git a/platform/gateway/gateway.yaml b/platform/gateway/gateway.yaml new file mode 100644 index 0000000..5a4bc80 --- /dev/null +++ b/platform/gateway/gateway.yaml @@ -0,0 +1,42 @@ +--- +# Shared Gateway — HTTPS listeners on *.home.keithjackson.dev, TLS terminated with the wildcard cert +# (docs/09). Implemented by Cilium (GatewayClass `cilium`). The ingress VIP is announced via Cilium +# L2 announcement on the flat LAN (docs/09, OQ-1 resolved). Phase 0 STUB. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: shared + namespace: gateway +spec: + gatewayClassName: cilium + listeners: + - name: https + protocol: HTTPS + port: 443 + hostname: "*.home.keithjackson.dev" + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: wildcard-home-tls # produced by wildcard-certificate.yaml + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + # App namespaces opt in to attach HTTPRoutes here (WS-APPS renders the label + + # ReferenceGrant from ServiceDefinition `exposure`). docs/09. + gateway-access: "shared" + - name: http-redirect + protocol: HTTP + port: 80 + hostname: "*.home.keithjackson.dev" + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + gateway-access: "shared" +# TODO(WS-NET, Phase 2): pin the L2-announced VIP / IP pool; add HTTP->HTTPS redirect HTTPRoute; +# confirm cross-namespace ReferenceGrant pattern with WS-APPS. Ingress opens NORTH-SOUTH only — +# it does not relax east-west default-deny (docs/09). diff --git a/platform/gateway/gatewayclass.yaml b/platform/gateway/gatewayclass.yaml new file mode 100644 index 0000000..d96c9cd --- /dev/null +++ b/platform/gateway/gatewayclass.yaml @@ -0,0 +1,9 @@ +--- +# GatewayClass implemented by Cilium (ADR-0002: Gateway API via Cilium, no separate ingress +# controller). Phase 0 STUB. The controllerName is provided by the Cilium Gateway API integration. +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: cilium +spec: + controllerName: io.cilium/gateway-controller diff --git a/platform/gateway/kustomization.yaml b/platform/gateway/kustomization.yaml new file mode 100644 index 0000000..4078f92 --- /dev/null +++ b/platform/gateway/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - gatewayclass.yaml + - wildcard-certificate.yaml + - gateway.yaml diff --git a/platform/gateway/namespace.yaml b/platform/gateway/namespace.yaml new file mode 100644 index 0000000..5baf7a0 --- /dev/null +++ b/platform/gateway/namespace.yaml @@ -0,0 +1,7 @@ +--- +# Shared Gateway lives in its own namespace; HTTPRoutes from app namespaces attach cross-namespace +# via ReferenceGrant (rendered by WS-APPS from ServiceDefinitions). +apiVersion: v1 +kind: Namespace +metadata: + name: gateway diff --git a/platform/gateway/wildcard-certificate.yaml b/platform/gateway/wildcard-certificate.yaml new file mode 100644 index 0000000..12a5bbe --- /dev/null +++ b/platform/gateway/wildcard-certificate.yaml @@ -0,0 +1,18 @@ +--- +# Wildcard certificate `*.home.keithjackson.dev`, terminated at the shared Gateway (ADR-0006 default). +# Makes onboarding certificate-free. Issued by the env-selected ClusterIssuer (R-ENV-02). +# Phase 0 STUB — cannot issue without a cluster + Cloudflare token. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: wildcard-home + namespace: gateway +spec: + secretName: wildcard-home-tls # consumed by the Gateway HTTPS listener below + dnsNames: + - "*.home.keithjackson.dev" + - "home.keithjackson.dev" # apex for the C2 UI (docs/09) + issuerRef: + kind: ClusterIssuer + # ${ACME_CLUSTER_ISSUER}: dev -> letsencrypt-staging, prod -> letsencrypt-prod (cluster-vars). + name: ${ACME_CLUSTER_ISSUER} diff --git a/platform/image-automation/README.md b/platform/image-automation/README.md new file mode 100644 index 0000000..58f707e --- /dev/null +++ b/platform/image-automation/README.md @@ -0,0 +1,29 @@ +# platform/image-automation + +**Flux image-update-automation** — hands-off container image tag advances driven by declared policy. + +- **Governing ADR:** [ADR-0009 — Automated updates: Flux image automation + Renovate](../../../home-network-k3s/decisions/adr-0009-automated-image-updates.md) +- **Spec:** [docs/11 — Automated Updates](../../../home-network-k3s/docs/11-automated-updates.md), + [docs/05 §Automated updates interplay](../../../home-network-k3s/docs/05-gitops-and-delivery.md) +- **Serves:** R-UPD-02 (with R-UPD-01/03 safety rails) +- **Owner:** WS-UPDATE (mechanism), WS-GITOPS (policy rendering from ServiceDefinition) + +## What this stub is + +The **cluster-wide** `ImageUpdateAutomation` that scans registries and **commits the resolved version +back to git** (machine-authored, auditable — ADR-0009). No human or AI authors a tag bump (R-UPD-02). + +## Scope boundary (contract) + +This dir holds **only** the cluster-wide automation + git write-back settings. **Per-service** +`ImageRepository` / `ImagePolicy` (semver range / stable channel) are rendered from each +ServiceDefinition's `update` block by WS-GITOPS/WS-UPDATE and live with `apps/` — the "Update policy +contract" in `roadmap/workstreams.md`. **Helm chart / manifest bumps** are Renovate's job (separate), +not this controller's. + +## TODO (later phases — Phase 4) + +- Confirm write-back strategy (direct commit to tracked branch vs. Renovate-style PRs). +- Wire the safety rails that are **acceptance gates** (ADR-0009): stable-only, health-gated rollout, + auto-rollback on failed health, `pin` escape hatch, alert on update failure. +- Requires the Flux **image-reflector** + **image-automation** controllers (Flux bootstrap component). diff --git a/platform/image-automation/image-update-automation.yaml b/platform/image-automation/image-update-automation.yaml new file mode 100644 index 0000000..fa48602 --- /dev/null +++ b/platform/image-automation/image-update-automation.yaml @@ -0,0 +1,39 @@ +--- +# Flux image-update-automation — the CLUSTER-WIDE write-back engine for hands-off image tag advances +# (ADR-0009, R-UPD-02). It scans registries per each service's ImagePolicy and commits the resolved +# version back to git — no human/AI authors the bump. Phase 0 STUB. +# +# SCOPE: this dir holds ONLY the cluster-wide ImageUpdateAutomation + git write-back settings. +# Per-service `ImageRepository`/`ImagePolicy` are rendered from each ServiceDefinition's `update` +# block (WS-GITOPS/WS-UPDATE) and live with apps/ — NOT here. (Contract: roadmap/workstreams.md +# "Update policy contract".) +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageUpdateAutomation +metadata: + name: flux-image-automation + namespace: flux-system +spec: + interval: 30m # the "schedule" on which stable versions are picked up (R-UPD-02) + sourceRef: + kind: GitRepository + name: flux-system + git: + checkout: + ref: + branch: main # TODO(WS-UPDATE): confirm write-back branch (may open PRs instead) + commit: + author: + name: fluxcdbot # machine author — the auditable, intended write-back (ADR-0009) + email: fluxcdbot@users.noreply.home.keithjackson.dev + messageTemplate: | + chore(images): automated update + + Machine-authored image tag advance per ImagePolicy (R-UPD-02, ADR-0009). + push: + branch: main + update: + strategy: Setters # image-automation setter markers in per-service manifests (apps/) + path: ./apps +# TODO(WS-UPDATE, Phase 4): safety rails are acceptance gates (ADR-0009) — stable-only, health-gated +# rollout, auto-rollback on failed health, pin escape hatch, alert on update failure. Prove a test +# service auto-advances with no human/AI edit; a bad update auto-rolls-back + alerts (R-UPD-01/02/03). diff --git a/platform/image-automation/kustomization.yaml b/platform/image-automation/kustomization.yaml new file mode 100644 index 0000000..e00a03c --- /dev/null +++ b/platform/image-automation/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - image-update-automation.yaml diff --git a/platform/keycloak/README.md b/platform/keycloak/README.md new file mode 100644 index 0000000..4602085 --- /dev/null +++ b/platform/keycloak/README.md @@ -0,0 +1,30 @@ +# platform/keycloak + +**Keycloak** — the platform Identity Provider (SSO via OIDC + SAML). + +- **Governing ADR:** [ADR-0014 — IdP: Keycloak](../../../home-network-k3s/decisions/adr-0014-idp-keycloak.md) +- **Spec:** [docs/15 — Identity & Access](../../../home-network-k3s/docs/15-identity-and-access.md) +- **Serves:** R-IDP-01..06 +- **Owner:** WS-SEC + +## What this stub is + +Flux `HelmRelease` for the Keycloak instance with **PostgreSQL on Longhorn** (DR-critical). Replica +count is an overlay knob (`${KEYCLOAK_REPLICAS}`, R-ENV-02). Admin/DB credentials referenced **by +name**, **SOPS-encrypted** (WS-SEC). Realms/clients/groups are config-as-code (Phase 6). + +> The ADR allows the **Keycloak Operator OR Helm** for the instance; this stub uses Helm — swap to +> the Operator if WS-SEC prefers CR-driven config. + +## Contract (SSO/auth) + +The "SSO/auth contract" (`roadmap/workstreams.md`, WS-SEC): a ServiceDefinition's `auth` block maps to +a Keycloak client (native OIDC) or oauth2-proxy protection (forward-auth — see +[`../oauth2-proxy`](../oauth2-proxy)). Consumed by WS-APPS/WS-C2. + +## TODO (later phases — onboarded EARLY in Phase 6) + +- Realms/clients/groups as code (keycloak-config-cli / realm-import); MFA for privileged groups. +- **DR-critical:** off-cluster DB backup (velero/) + documented **break-glass** local-admin path. +- Exceptions that keep native auth (do not force SSO): Plex/Seerr (Plex auth), Minecraft + (Mojang/Microsoft), UniFi (native) — ADR-0014. diff --git a/platform/keycloak/helmrelease.yaml b/platform/keycloak/helmrelease.yaml new file mode 100644 index 0000000..ee45f4f --- /dev/null +++ b/platform/keycloak/helmrelease.yaml @@ -0,0 +1,40 @@ +--- +# Keycloak — platform IdP (OIDC + SAML), config-as-code, PostgreSQL on Longhorn (ADR-0014). +# Phase 0 STUB. DR-CRITICAL: DB backed up off-cluster; documented break-glass local-admin path. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: keycloak + namespace: keycloak +spec: + interval: 30m + chart: + spec: + chart: keycloak + version: ">=24.0.0 <25.0.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: bitnami + namespace: keycloak + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + replicaCount: ${KEYCLOAK_REPLICAS} # overlay knob: dev 1 / prod 2 (HA) — R-ENV-02 + production: true # TLS/proxy-aware; served behind the Gateway (public) + proxyHeaders: xforwarded + # PostgreSQL on Longhorn (ADR-0014) — DR-critical, backed up off-cluster (velero/). + postgresql: + enabled: true + primary: + persistence: + storageClass: longhorn + # Admin + DB credentials: SOPS-encrypted secrets referenced BY NAME only (WS-SEC, ADR-0007). + auth: + existingSecret: keycloak-admin-credentials + # TODO(WS-SEC, Phase 6): realms/clients/groups as code (keycloak-config-cli / realm-import); + # MFA for privileged groups; break-glass local-admin path (e.g. on Gitea). Onboarded EARLY + # (with PiHole/Gitea) so later services adopt SSO as they come up (roadmap Phase 6). diff --git a/platform/keycloak/helmrepository.yaml b/platform/keycloak/helmrepository.yaml new file mode 100644 index 0000000..2fef077 --- /dev/null +++ b/platform/keycloak/helmrepository.yaml @@ -0,0 +1,11 @@ +--- +# TODO(WS-SEC): the ADR permits Keycloak Operator OR Helm for the instance. This stub uses a Helm +# chart source; swap to the Keycloak Operator if WS-SEC prefers CR-driven config. (ADR-0014) +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: bitnami + namespace: keycloak +spec: + interval: 1h + url: https://charts.bitnami.com/bitnami diff --git a/platform/keycloak/kustomization.yaml b/platform/keycloak/kustomization.yaml new file mode 100644 index 0000000..5704992 --- /dev/null +++ b/platform/keycloak/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml diff --git a/platform/keycloak/namespace.yaml b/platform/keycloak/namespace.yaml new file mode 100644 index 0000000..bf1caaf --- /dev/null +++ b/platform/keycloak/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: keycloak diff --git a/platform/keycloak/values.yaml b/platform/keycloak/values.yaml new file mode 100644 index 0000000..f55e434 --- /dev/null +++ b/platform/keycloak/values.yaml @@ -0,0 +1,8 @@ +# Keycloak Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# replicaCount: ${KEYCLOAK_REPLICAS} # overlay knob (dev 1 / prod 2 HA) +# postgresql on longhorn # DR-critical (ADR-0014), backed up off-cluster +# auth.existingSecret: keycloak-admin-credentials (SOPS-encrypted, WS-SEC — name only) +# realm/clients/groups as code # keycloak-config-cli / realm-import (Phase 6) +# public (hardened) exposure so login flows work for internet-exposed services (ADR-0014). +# Exceptions (do NOT force SSO): Plex/Seerr (Plex auth), Minecraft (Mojang), UniFi (native). diff --git a/platform/kubevirt/README.md b/platform/kubevirt/README.md new file mode 100644 index 0000000..e46f517 --- /dev/null +++ b/platform/kubevirt/README.md @@ -0,0 +1,31 @@ +# platform/kubevirt + +**KubeVirt + CDI** — run application VMs (IaaS) as Kubernetes resources on the same control plane, +network, storage, and GitOps flow as containers. + +- **Governing ADR:** [ADR-0005 — VM workloads: KubeVirt + CDI](../../../home-network-k3s/decisions/adr-0005-vm-workloads-kubevirt.md) +- **Spec:** [docs/06 — VM Workloads (KubeVirt)](../../../home-network-k3s/docs/06-vm-workloads-kubevirt.md) +- **Serves:** R-PLAT-03/04, R-STATE-01 +- **Owner:** WS-VM + +## What this stub is + +A Kustomization that will pull the pinned **KubeVirt operator** + **CDI operator** release manifests +(commented TODO — added in Phase 5), plus the **`KubeVirt`** and **`CDI`** custom resources that tune +them. VM disks land on **Longhorn** PVCs via CDI; VMs inherit Cilium default-deny + egress classes. + +> **Layer distinction (docs/01):** these are **Layer-2 application VMs** (a feature the platform +> offers), NOT the Layer-1 cluster-node VMs (which WS-INFRA provisions with OpenTofu/libvirt). Do not +> conflate them. + +## Cross-workstream prerequisite + +Nodes must expose **virtualization extensions** (nested virt in dev KVMs; native on prod) — a node +baseline prerequisite owned by **WS-INFRA** (docs/02). Coordinate before Phase 5. + +## TODO (later phases — Phase 5, gated on Phases 2–3) + +- Add pinned operator release manifests as Kustomize bases; let Renovate maintain versions. +- Run one representative application VM via a ServiceDefinition `kind: vm`; prove create/start/stop/ + destroy via `kubectl`/`virtctl`, Longhorn-backed disk survives rebuild, default-deny + egress apply, + guest auto-updates (Phase-5 exit gates). diff --git a/platform/kubevirt/cdi-cr.yaml b/platform/kubevirt/cdi-cr.yaml new file mode 100644 index 0000000..3af9c95 --- /dev/null +++ b/platform/kubevirt/cdi-cr.yaml @@ -0,0 +1,11 @@ +--- +# CDI CR — Containerized Data Importer, provisions VM disks onto Longhorn PVCs (ADR-0005). +# Phase 0 STUB; requires the CDI operator (added in Phase 5). +apiVersion: cdi.kubevirt.io/v1beta1 +kind: CDI +metadata: + name: cdi + namespace: cdi +spec: + # TODO(WS-VM, Phase 5): scratch space storageClass (longhorn), upload/import proxy settings. + config: {} diff --git a/platform/kubevirt/kubevirt-cr.yaml b/platform/kubevirt/kubevirt-cr.yaml new file mode 100644 index 0000000..d7e6caf --- /dev/null +++ b/platform/kubevirt/kubevirt-cr.yaml @@ -0,0 +1,16 @@ +--- +# KubeVirt CR — tunes the KubeVirt install (ADR-0005). Phase 0 STUB; requires the KubeVirt operator +# (added in Phase 5) and virtualization extensions on the nodes (nested virt in dev KVMs — a node +# baseline prerequisite from docs/02, owned by WS-INFRA). +apiVersion: kubevirt.io/v1 +kind: KubeVirt +metadata: + name: kubevirt + namespace: kubevirt +spec: + configuration: + developerConfiguration: + # TODO(WS-VM/WS-INFRA, Phase 5): enable only if hardware virt is unavailable in dev KVMs; + # prefer real (nested) virt so the IaaS path mirrors prod. + featureGates: [] + # TODO(WS-VM, Phase 5): workload placement, live-migration, evictionStrategy for the 3-node cluster. diff --git a/platform/kubevirt/kustomization.yaml b/platform/kubevirt/kustomization.yaml new file mode 100644 index 0000000..601d4b7 --- /dev/null +++ b/platform/kubevirt/kustomization.yaml @@ -0,0 +1,13 @@ +--- +# KubeVirt + CDI. Installed via their OPERATORS (plain release manifests), then the KubeVirt/CDI +# custom resources tune the deployment. Phase 0 STUB. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespaces.yaml + # TODO(WS-VM, Phase 5): add the pinned operator release manifests, e.g. + # - https://github.com/kubevirt/kubevirt/releases/download/vX.Y.Z/kubevirt-operator.yaml + # - https://github.com/kubevirt/containerized-data-importer/releases/download/vX.Y.Z/cdi-operator.yaml + # (Flux can pull remote bases; pin versions and let Renovate maintain them.) + - kubevirt-cr.yaml + - cdi-cr.yaml diff --git a/platform/kubevirt/namespaces.yaml b/platform/kubevirt/namespaces.yaml new file mode 100644 index 0000000..d060d6e --- /dev/null +++ b/platform/kubevirt/namespaces.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kubevirt +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cdi diff --git a/platform/kustomization.yaml b/platform/kustomization.yaml new file mode 100644 index 0000000..1a1e80e --- /dev/null +++ b/platform/kustomization.yaml @@ -0,0 +1,25 @@ +--- +# Top-level platform kustomization — the entrypoint the Flux `platform` Kustomization renders +# (clusters/base/platform.yaml, path: ./platform). +# +# Phase 0 scaffolding: lists every platform component EXCEPT kyverno (owned by the policy engineer, +# WS-TEST — see platform/README.md). +# +# ⚠️ INTEGRATION TODO(PM): once platform/kyverno/ is authored by the policy engineer, uncomment the +# `- kyverno` line below. Keep it FIRST so admission policies are present before other components +# reconcile. This workstream must NOT create files under platform/kyverno/. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + # - kyverno # TODO(PM): enable when the policy engineer's kyverno stub lands (WS-TEST) + - cilium + - longhorn + - cert-manager + - externaldns + - gateway + - monitoring + - velero + - kubevirt + - image-automation + - keycloak + - oauth2-proxy diff --git a/platform/longhorn/README.md b/platform/longhorn/README.md new file mode 100644 index 0000000..85c45ab --- /dev/null +++ b/platform/longhorn/README.md @@ -0,0 +1,31 @@ +# platform/longhorn + +**Longhorn** — distributed replicated block storage across the 3 nodes. + +- **Governing ADR:** [ADR-0003 — Distributed storage: Longhorn (+ bulk tier + Velero)](../../../home-network-k3s/decisions/adr-0003-storage-longhorn.md) +- **Spec:** [docs/04 — Storage & State](../../../home-network-k3s/docs/04-storage-and-state.md) +- **Serves:** R-STATE-01..04 +- **Owner:** WS-STORAGE + +## What this stub is + +Flux `HelmRelease` + two `StorageClass`es: +- **`longhorn`** — the **default**, **3-replica** replicated class (replica count is the + `${LONGHORN_DEFAULT_REPLICA_COUNT}` overlay knob). Durable home for app/config state and KubeVirt + VM disks (via CDI). +- **`bulk`** — non-replicated tier for large media (Plex library, download staging). **STUB**: its + backing store is **OQ-2 (unresolved)** — escalate before finalizing. Plex's metadata DB stays on + the 3-replica class regardless (ADR-0003). + +## Contract (consumed by everyone with state) + +Per `roadmap/workstreams.md`, WS-STORAGE exposes the `longhorn` (replicated) and `bulk` (media) +StorageClasses; WS-APPS/WS-VM/WS-OBS reference them from ServiceDefinitions. Do not place bulk media +on the replicated tier or app DBs on the bulk tier. + +## TODO (later phases) + +- **OQ-2 (human):** decide the bulk-tier backing (NAS/NFS vs dedicated disk). Blocks `bulk` finalize. +- **Phase 2 (WS-STORAGE):** dedicated Longhorn data disk path (docs/02); prove a PVC survives + pod destroy/rebuild (R-STATE-01, Phase-2 exit gate). +- **Phase 4 (WS-STORAGE):** wire off-cluster backup target with Velero (R-STATE-03). diff --git a/platform/longhorn/helmrelease.yaml b/platform/longhorn/helmrelease.yaml new file mode 100644 index 0000000..83b2e71 --- /dev/null +++ b/platform/longhorn/helmrelease.yaml @@ -0,0 +1,36 @@ +--- +# Longhorn — distributed replicated block storage (ADR-0003). Phase 0 STUB. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: longhorn + namespace: longhorn-system +spec: + interval: 30m + chart: + spec: + chart: longhorn + version: ">=1.7.0 <1.8.0" # TODO(WS-STORAGE): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: longhorn + namespace: longhorn-system + install: + createNamespace: false + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + # We define StorageClasses ourselves (storageclasses.yaml) so the default 3-replica SC and the + # bulk tier are explicit and GitOps-managed. Disable the chart's built-in default SC. + persistence: + defaultClass: false + # ${LONGHORN_DEFAULT_REPLICA_COUNT} resolved from cluster-vars (R-ENV-02). + defaultClassReplicaCount: ${LONGHORN_DEFAULT_REPLICA_COUNT} + defaultSettings: + # TODO(WS-STORAGE, Phase 2): set defaultDataPath to the dedicated Longhorn data disk + # (docs/02 — a dedicated data disk is presented to Longhorn on each node). + defaultDataPath: "/var/lib/longhorn" + # TODO(WS-STORAGE, Phase 4): configure off-cluster backup target here (see velero/ + ADR-0003). diff --git a/platform/longhorn/helmrepository.yaml b/platform/longhorn/helmrepository.yaml new file mode 100644 index 0000000..7efdf6a --- /dev/null +++ b/platform/longhorn/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: longhorn + namespace: longhorn-system +spec: + interval: 1h + url: https://charts.longhorn.io diff --git a/platform/longhorn/kustomization.yaml b/platform/longhorn/kustomization.yaml new file mode 100644 index 0000000..a567a3c --- /dev/null +++ b/platform/longhorn/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml + - storageclasses.yaml diff --git a/platform/longhorn/namespace.yaml b/platform/longhorn/namespace.yaml new file mode 100644 index 0000000..e65bc72 --- /dev/null +++ b/platform/longhorn/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: longhorn-system diff --git a/platform/longhorn/storageclasses.yaml b/platform/longhorn/storageclasses.yaml new file mode 100644 index 0000000..29e79f7 --- /dev/null +++ b/platform/longhorn/storageclasses.yaml @@ -0,0 +1,34 @@ +--- +# Default replicated StorageClass — 3 replicas (ADR-0003). The durable home for app/config state +# and KubeVirt VM disks. Replica count is an overlay knob (R-ENV-02). +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: longhorn + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: driver.longhorn.io +allowVolumeExpansion: true +reclaimPolicy: Delete +volumeBindingMode: Immediate +parameters: + numberOfReplicas: "${LONGHORN_DEFAULT_REPLICA_COUNT}" # from cluster-vars (default 3) + staleReplicaTimeout: "30" + fsType: "ext4" +--- +# Bulk media tier — large, NON-replicated data (Plex library, download staging). ADR-0003 / docs/04. +# NOTE: backing store (NAS/NFS vs dedicated large disk) is OQ-2, unresolved — this is a STUB. +# Plex's metadata DB stays on the 3-replica `longhorn` class regardless (ADR-0003). +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: bulk +provisioner: driver.longhorn.io # TODO(WS-STORAGE): may become an NFS provisioner per OQ-2 +allowVolumeExpansion: true +reclaimPolicy: Retain +volumeBindingMode: Immediate +parameters: + numberOfReplicas: "1" # non-replicated bulk tier (capacity, not durability) + staleReplicaTimeout: "30" + fsType: "ext4" +# TODO(WS-STORAGE): finalize once OQ-2 (bulk backing) is answered by the human operator. diff --git a/platform/longhorn/values.yaml b/platform/longhorn/values.yaml new file mode 100644 index 0000000..6ec48c2 --- /dev/null +++ b/platform/longhorn/values.yaml @@ -0,0 +1,7 @@ +# Longhorn Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# persistence.defaultClass: false # we own StorageClasses (storageclasses.yaml) +# persistence.defaultClassReplicaCount: ${LONGHORN_DEFAULT_REPLICA_COUNT} # overlay knob +# defaultSettings.defaultDataPath: # TODO(WS-STORAGE): dedicated Longhorn disk (docs/02) +# defaultSettings.backupTarget: # TODO(WS-STORAGE, Phase 4): DR target (ADR-0003) +# Longhorn UI exposure gated behind oauth2-proxy forward-auth (ADR-0014). diff --git a/platform/monitoring/README.md b/platform/monitoring/README.md new file mode 100644 index 0000000..f2ea0f9 --- /dev/null +++ b/platform/monitoring/README.md @@ -0,0 +1,31 @@ +# platform/monitoring + +**Observability stack** — kube-prometheus-stack (Prometheus + Alertmanager + Grafana) + **Loki** + +**Grafana Alloy**. + +- **Spec:** [docs/10 — Observability & Alerting](../../../home-network-k3s/docs/10-observability-and-alerting.md) + (named in [docs/01](../../../home-network-k3s/docs/01-architecture-overview.md); no dedicated ADR) +- **Serves:** R-OBS-01..04 +- **Owner:** WS-OBS + +## What this stub is + +Three Flux `HelmRelease`s (kube-prometheus-stack, loki, alloy) with placeholder values. Retention and +PVC sizes are overlay knobs (`${MONITORING_*}`, R-ENV-02): short/small in dev, long/large in prod. +Persistent data uses the replicated `longhorn` StorageClass. + +## Contract (observability) + +The "observability contract" (`roadmap/workstreams.md`): how a service declares metrics/SLIs/alerts +and gets dashboards. ServiceMonitors/dashboards are rendered from ServiceDefinitions by WS-APPS/WS-VM +against this stack. Grafana is part of the C2 UI composition (docs/12), behind Keycloak SSO +(ADR-0014). + +## TODO (later phases) + +- **Phase 4 (WS-OBS):** Alertmanager → Gmail SMTP receiver (creds SOPS-encrypted, name only); + Loki retention via compactor (`retention_enabled: true` — a period alone does nothing); Alloy log + scrape config → Loki; platform-health alerts. **Reconciliation errors + drift are alerting + conditions (R-OBS-04).** +- **Exit gate (Phase 4):** an injected fault emails an alert; dashboards render (R-OBS-01/02/03/04). +- **Grafana OIDC** client via Keycloak (WS-SEC/WS-C2). diff --git a/platform/monitoring/alloy.yaml b/platform/monitoring/alloy.yaml new file mode 100644 index 0000000..18cb818 --- /dev/null +++ b/platform/monitoring/alloy.yaml @@ -0,0 +1,25 @@ +--- +# Grafana Alloy — telemetry collector / log shipping to Loki (docs/10). Phase 0 STUB. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: alloy + namespace: monitoring +spec: + interval: 30m + chart: + spec: + chart: alloy + version: ">=0.10.0 <1.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: grafana + namespace: monitoring + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: {} + # TODO(WS-OBS, Phase 4): Alloy config to scrape pod/node logs -> Loki; K8s events; metrics relay. diff --git a/platform/monitoring/helmrepositories.yaml b/platform/monitoring/helmrepositories.yaml new file mode 100644 index 0000000..2eb9949 --- /dev/null +++ b/platform/monitoring/helmrepositories.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: prometheus-community + namespace: monitoring +spec: + interval: 1h + url: https://prometheus-community.github.io/helm-charts +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: grafana + namespace: monitoring +spec: + interval: 1h + url: https://grafana.github.io/helm-charts diff --git a/platform/monitoring/kube-prometheus-stack.yaml b/platform/monitoring/kube-prometheus-stack.yaml new file mode 100644 index 0000000..c0ad1a2 --- /dev/null +++ b/platform/monitoring/kube-prometheus-stack.yaml @@ -0,0 +1,44 @@ +--- +# kube-prometheus-stack — Prometheus + Alertmanager + Grafana (docs/10). Phase 0 STUB. +# Retention/sizes are overlay knobs (R-ENV-02). Alertmanager emails via SMTP. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: kube-prometheus-stack + namespace: monitoring +spec: + interval: 30m + chart: + spec: + chart: kube-prometheus-stack + version: ">=65.0.0 <66.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: prometheus-community + namespace: monitoring + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + prometheus: + prometheusSpec: + retention: ${MONITORING_PROMETHEUS_RETENTION} # overlay knob (dev short / prod long) + storageSpec: + volumeClaimTemplate: + spec: + storageClassName: longhorn # replicated tier (ADR-0003) + resources: + requests: + storage: ${MONITORING_PROMETHEUS_PVC_SIZE} + grafana: + # Grafana behind Keycloak SSO (native OIDC) — ADR-0014. C2 UI composition (docs/12). + # TODO(WS-SEC/WS-C2): OIDC client + admin password from SOPS secret (name only). + enabled: true + alertmanager: + alertmanagerSpec: {} + # TODO(WS-OBS, Phase 4): Alertmanager → Gmail SMTP receiver; SMTP creds SOPS-encrypted (name + # only). Reconciliation errors + drift are alerting conditions (R-OBS-04). Prove: injected + # fault emails an alert (Phase-4 exit gate). diff --git a/platform/monitoring/kustomization.yaml b/platform/monitoring/kustomization.yaml new file mode 100644 index 0000000..3aa2196 --- /dev/null +++ b/platform/monitoring/kustomization.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepositories.yaml + - kube-prometheus-stack.yaml + - loki.yaml + - alloy.yaml diff --git a/platform/monitoring/loki.yaml b/platform/monitoring/loki.yaml new file mode 100644 index 0000000..902f936 --- /dev/null +++ b/platform/monitoring/loki.yaml @@ -0,0 +1,33 @@ +--- +# Loki — log aggregation (docs/10). Phase 0 STUB. PVC size is an overlay knob (R-ENV-02). +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: loki + namespace: monitoring +spec: + interval: 30m + chart: + spec: + chart: loki + version: ">=6.0.0 <7.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: grafana + namespace: monitoring + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + # TODO(WS-OBS, Phase 4): single-binary vs scalable deployment mode; retention via compactor + # (retention_enabled: true — a period alone does nothing); storage backing. + loki: + storage: + type: filesystem + singleBinary: + persistence: + storageClass: longhorn + size: ${MONITORING_LOKI_PVC_SIZE} diff --git a/platform/monitoring/namespace.yaml b/platform/monitoring/namespace.yaml new file mode 100644 index 0000000..ff7ae1b --- /dev/null +++ b/platform/monitoring/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring diff --git a/platform/oauth2-proxy/README.md b/platform/oauth2-proxy/README.md new file mode 100644 index 0000000..abd9bb5 --- /dev/null +++ b/platform/oauth2-proxy/README.md @@ -0,0 +1,28 @@ +# platform/oauth2-proxy + +**oauth2-proxy** — forward-auth at the Gateway for apps **without usable OIDC**, authenticating +against Keycloak. + +- **Governing ADR:** [ADR-0014 — IdP: Keycloak](../../../home-network-k3s/decisions/adr-0014-idp-keycloak.md) +- **Spec:** [docs/15 — Identity & Access](../../../home-network-k3s/docs/15-identity-and-access.md) +- **Serves:** R-IDP-01..06 (esp. R-IDP-02: management UIs never unauthenticated) +- **Owner:** WS-SEC (Gateway wiring shared with WS-NET) + +## What this stub is + +Flux `HelmRelease` for oauth2-proxy as the forward-auth provider for non-OIDC apps (PiHole, *arr UIs, +qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI — ADR-0014). Replica count is an overlay +knob (`${OAUTH2_PROXY_REPLICAS}`, R-ENV-02). Client/cookie secrets referenced **by name**, +**SOPS-encrypted** (WS-SEC). + +## Contract (SSO/auth + Gateway) + +Pairs with [`../keycloak`](../keycloak) to realize the "SSO/auth contract": a ServiceDefinition `auth: +forward-auth` service is protected by oauth2-proxy at the Gateway (ext-auth); `auth: oidc` services go +native to Keycloak. Gateway ext-auth wiring is a **WS-NET** contract. + +## TODO (later phases — Phase 6) + +- OIDC issuer URL to the Keycloak realm; client registration. +- Gateway forward-auth / ext-authz wiring (WS-NET); group-based authorization; MFA enforced in + Keycloak upstream. diff --git a/platform/oauth2-proxy/helmrelease.yaml b/platform/oauth2-proxy/helmrelease.yaml new file mode 100644 index 0000000..8514306 --- /dev/null +++ b/platform/oauth2-proxy/helmrelease.yaml @@ -0,0 +1,38 @@ +--- +# oauth2-proxy — forward-auth at the Gateway for apps WITHOUT usable OIDC (PiHole, *arr UIs, +# qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI). Authenticates against Keycloak. +# ADR-0014. Phase 0 STUB. Replica count is an overlay knob (R-ENV-02). +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: oauth2-proxy + namespace: oauth2-proxy +spec: + interval: 30m + chart: + spec: + chart: oauth2-proxy + version: ">=7.0.0 <8.0.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: oauth2-proxy + namespace: oauth2-proxy + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + replicaCount: ${OAUTH2_PROXY_REPLICAS} # overlay knob: dev 1 / prod 2 (HA) — R-ENV-02 + config: + # OIDC provider = Keycloak. Client id/secret + cookie secret: SOPS-encrypted secret, name only. + existingSecret: oauth2-proxy-credentials + extraArgs: + provider: oidc + # TODO(WS-SEC, Phase 6): oidc-issuer-url = https://keycloak.home.keithjackson.dev/realms/ + oidc-issuer-url: "REPLACE-ME" + email-domain: "*" + # Forward-auth integration wired at the Gateway (HTTPRoute + ext-auth). Contract with WS-NET. + # TODO(WS-SEC/WS-NET, Phase 6): Gateway forward-auth/ext-authz wiring; group-based authorization; + # MFA enforced upstream in Keycloak. Management UIs must never be unauthenticated (R-IDP-02). diff --git a/platform/oauth2-proxy/helmrepository.yaml b/platform/oauth2-proxy/helmrepository.yaml new file mode 100644 index 0000000..2b627e7 --- /dev/null +++ b/platform/oauth2-proxy/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: oauth2-proxy + namespace: oauth2-proxy +spec: + interval: 1h + url: https://oauth2-proxy.github.io/manifests diff --git a/platform/oauth2-proxy/kustomization.yaml b/platform/oauth2-proxy/kustomization.yaml new file mode 100644 index 0000000..5704992 --- /dev/null +++ b/platform/oauth2-proxy/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml diff --git a/platform/oauth2-proxy/namespace.yaml b/platform/oauth2-proxy/namespace.yaml new file mode 100644 index 0000000..5b602ca --- /dev/null +++ b/platform/oauth2-proxy/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: oauth2-proxy diff --git a/platform/oauth2-proxy/values.yaml b/platform/oauth2-proxy/values.yaml new file mode 100644 index 0000000..9746fdf --- /dev/null +++ b/platform/oauth2-proxy/values.yaml @@ -0,0 +1,7 @@ +# oauth2-proxy Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# replicaCount: ${OAUTH2_PROXY_REPLICAS} # overlay knob (dev 1 / prod 2 HA) +# provider: oidc, oidc-issuer-url = Keycloak realm (Phase 6) +# config.existingSecret: oauth2-proxy-credentials (client id/secret + cookie secret; SOPS — name only) +# Forward-auth wired at the Gateway (ext-auth) for non-OIDC apps (ADR-0014) — contract with WS-NET. +# Protects: PiHole, *arr UIs, qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI. diff --git a/platform/velero/README.md b/platform/velero/README.md new file mode 100644 index 0000000..b2136bb --- /dev/null +++ b/platform/velero/README.md @@ -0,0 +1,22 @@ +# platform/velero + +**Velero** — off-cluster backup and DR of volumes + Kubernetes resource state. + +- **Governing ADR:** [ADR-0003 — Longhorn (+ bulk tier + Velero)](../../../home-network-k3s/decisions/adr-0003-storage-longhorn.md) +- **Spec:** [docs/04 — Storage & State](../../../home-network-k3s/docs/04-storage-and-state.md) +- **Serves:** R-STATE-03 +- **Owner:** WS-STORAGE + +## What this stub is + +Flux `HelmRelease` with a placeholder off-cluster backup target (S3-compatible/NFS) and scheduled +backups whose cadence/retention are overlay knobs (`${VELERO_*}`, R-ENV-02). Backup credentials are +referenced **by name** (`velero-backup-credentials`), **SOPS-encrypted** (WS-SEC). Works with Longhorn +volume backups. + +## TODO (later phases) + +- **OQ-2 (human):** concrete off-cluster target (endpoint, bucket, resilience) is a prerequisite. +- **Phase 4 (WS-STORAGE):** wire the target + Longhorn backups; prove a **DR drill** — restore a + namespace + volumes from off-cluster backup (R-STATE-03, Phase-4 exit gate). +- **DR-critical services** (Gitea, Keycloak, Nextcloud) get off-cluster backup + mirror per docs/05. diff --git a/platform/velero/helmrelease.yaml b/platform/velero/helmrelease.yaml new file mode 100644 index 0000000..90e8951 --- /dev/null +++ b/platform/velero/helmrelease.yaml @@ -0,0 +1,46 @@ +--- +# Velero — off-cluster backup/DR of volumes + resource state (ADR-0003, R-STATE-03). Phase 0 STUB. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: velero + namespace: velero +spec: + interval: 30m + chart: + spec: + chart: velero + version: ">=8.0.0 <9.0.0" # TODO(WS-STORAGE): pin at bring-up; Renovate maintains + sourceRef: + kind: HelmRepository + name: vmware-tanzu + namespace: velero + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + # Off-cluster backup target — S3-compatible/NFS (ADR-0003). STUB: concrete target + creds are + # OQ-2 / prerequisites. Credentials referenced BY NAME only, SOPS-encrypted (WS-SEC). + configuration: + backupStorageLocation: + - name: default + provider: aws # TODO(WS-STORAGE): S3-compatible endpoint per OQ-2 + bucket: REPLACE-ME + config: + region: REPLACE-ME + s3Url: REPLACE-ME + s3ForcePathStyle: "true" + volumeSnapshotLocation: + - name: default + provider: REPLACE-ME # Longhorn CSI snapshots / Velero fs-backup + credentials: + existingSecret: velero-backup-credentials # SOPS-encrypted (WS-SEC) — name only + schedules: + daily: + schedule: "${VELERO_BACKUP_SCHEDULE}" # overlay knob (cron) + template: + ttl: "${VELERO_RETENTION}" # overlay knob (dev short / prod long) + # TODO(WS-STORAGE, Phase 4): tested restore path (DR drill) is an acceptance gate (R-STATE-03). diff --git a/platform/velero/helmrepository.yaml b/platform/velero/helmrepository.yaml new file mode 100644 index 0000000..5d82e53 --- /dev/null +++ b/platform/velero/helmrepository.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: vmware-tanzu + namespace: velero +spec: + interval: 1h + url: https://vmware-tanzu.github.io/helm-charts diff --git a/platform/velero/kustomization.yaml b/platform/velero/kustomization.yaml new file mode 100644 index 0000000..5704992 --- /dev/null +++ b/platform/velero/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - helmrepository.yaml + - helmrelease.yaml diff --git a/platform/velero/namespace.yaml b/platform/velero/namespace.yaml new file mode 100644 index 0000000..7dd5bd7 --- /dev/null +++ b/platform/velero/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: velero diff --git a/platform/velero/values.yaml b/platform/velero/values.yaml new file mode 100644 index 0000000..be3c7ba --- /dev/null +++ b/platform/velero/values.yaml @@ -0,0 +1,7 @@ +# Velero Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml. +# +# backupStorageLocation: off-cluster S3-compatible/NFS (ADR-0003; concrete target = OQ-2) +# volumeSnapshotLocation: Longhorn CSI snapshots / Velero fs-backup +# credentials.existingSecret: velero-backup-credentials (SOPS-encrypted, WS-SEC — name only) +# schedules.daily: ${VELERO_BACKUP_SCHEDULE} / ttl ${VELERO_RETENTION} (overlay knobs) +# DR drill (tested restore) is a Phase-4 acceptance gate (R-STATE-03).