diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c2469c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +# bootstrap secret values — delivered via Omni, never committed +omni/secrets.env diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..ac2cd7c --- /dev/null +++ b/.mise.toml @@ -0,0 +1,12 @@ +[tools] +helm = "3.17.0" +yq = "4" +kubectl = "1.34" + +[tasks."oci:generate-manifests"] +description = "Regenerate OCI (oci-talos) Talos inline-manifests: argocd + cilium + appset" +run = "omni/scripts/generate-manifests.sh oci-talos" + +[tasks."unraid:generate-manifests"] +description = "Regenerate unraid-lab Talos inline-manifests: argocd + appset" +run = "omni/scripts/generate-manifests.sh unraid-lab" diff --git a/apps/cert-manager/app.yaml b/apps/cert-manager/app.yaml new file mode 100644 index 0000000..4c9faf2 --- /dev/null +++ b/apps/cert-manager/app.yaml @@ -0,0 +1,6 @@ +# App coords consumed by the layering ApplicationSet. Values layer: +# values/base/cert-manager.yaml -> types// -> clusters// (deep-merge, later wins). +chart: cert-manager +repoURL: https://charts.jetstack.io +version: "v1.17.1" +namespace: cert-manager diff --git a/apps/cilium/app.yaml b/apps/cilium/app.yaml new file mode 100644 index 0000000..989eb87 --- /dev/null +++ b/apps/cilium/app.yaml @@ -0,0 +1,4 @@ +chart: cilium +repoURL: https://helm.cilium.io +version: "1.17.18" +namespace: kube-system diff --git a/appset.yaml b/appset.yaml new file mode 100644 index 0000000..35cf337 --- /dev/null +++ b/appset.yaml @@ -0,0 +1,85 @@ +# Layering ApplicationSet — the "chart-app" engine. +# +# SEEDED PER CLUSTER by the Talos inline manifests. The two `list` elements below +# (clusterName / clusterType) are the ONLY per-cluster difference — each cluster's +# inline manifest embeds this file with its own identity. This copy is the +# canonical reference; the identities here are placeholders. +# +# Selection (which apps run): three git generators — +# apps/*/app.yaml shared (every cluster) +# types//apps/*/app.yaml per cluster-TYPE +# clusters//apps/*/app.yaml per CLUSTER +# Values (override anything the chart exposes): deep-merge valueFiles — +# values/base/.yaml -> values/types//.yaml -> values/clusters//.yaml +# Custom ApplicationSets (e.g. arc-runners) are NOT chart-apps and run on the raw +# directory-sync lane (clusters//apps/*/application.yaml), not here. +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: apps + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - matrix: # shared apps + generators: + - list: + elements: + - clusterName: CLUSTER_NAME + clusterType: CLUSTER_TYPE + - git: + repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps + revision: HEAD + files: + - path: apps/*/app.yaml + - matrix: # per cluster-TYPE apps + generators: + - list: + elements: + - clusterName: CLUSTER_NAME + clusterType: CLUSTER_TYPE + - git: + repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps + revision: HEAD + files: + - path: types/{{.clusterType}}/apps/*/app.yaml + - matrix: # per CLUSTER apps + generators: + - list: + elements: + - clusterName: CLUSTER_NAME + clusterType: CLUSTER_TYPE + - git: + repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps + revision: HEAD + files: + - path: clusters/{{.clusterName}}/apps/*/app.yaml + template: + metadata: + name: "{{.path.basename}}" # app name = its directory name + spec: + project: default + sources: + - repoURL: "{{.repoURL}}" + chart: "{{.chart}}" + targetRevision: "{{.version}}" + helm: + ignoreMissingValueFiles: true # type/cluster value files are optional + valueFiles: + - $v/values/base/{{.path.basename}}.yaml + - $v/values/types/{{.clusterType}}/{{.path.basename}}.yaml + - $v/values/clusters/{{.clusterName}}/{{.path.basename}}.yaml + - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps + targetRevision: HEAD + ref: v + destination: + server: https://kubernetes.default.svc + namespace: "{{.namespace}}" + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/bootstrap/argocd-app-of-apps.yaml b/bootstrap/argocd-app-of-apps.yaml deleted file mode 100644 index 5999371..0000000 --- a/bootstrap/argocd-app-of-apps.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: app-of-apps - namespace: argocd - annotations: - # Synced by the inline root Application from omni/patches/inline-manifests.yaml. - argocd.argoproj.io/sync-wave: "-1" -spec: - project: default - source: - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - path: infrastructure - directory: - recurse: true - include: "*/application.yaml" - destination: - server: https://kubernetes.default.svc - namespace: argocd - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true diff --git a/clusters/unraid-lab/app-of-apps.yaml b/clusters/unraid-lab/app-of-apps.yaml deleted file mode 100644 index f460a12..0000000 --- a/clusters/unraid-lab/app-of-apps.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: unraid-lab-apps - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "-1" -spec: - # Cluster-scoped app-of-apps. The unraid-lab cluster's inline root Application - # syncs THIS in addition to bootstrap/ (shared infra). OCI does NOT sync this - # path, so unraid-lab-only apps (Harbor) never land on OCI. - project: default - source: - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - path: clusters/unraid-lab/apps - directory: - recurse: true - include: "*/application.yaml" - destination: - server: https://kubernetes.default.svc - namespace: argocd - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true diff --git a/clusters/unraid-lab/apps/arc-controller/app.yaml b/clusters/unraid-lab/apps/arc-controller/app.yaml new file mode 100644 index 0000000..8026a4f --- /dev/null +++ b/clusters/unraid-lab/apps/arc-controller/app.yaml @@ -0,0 +1,5 @@ +# OCI Helm chart (repoURL is an OCI registry). +chart: gha-runner-scale-set-controller +repoURL: ghcr.io/actions/actions-runner-controller-charts +version: "0.14.2" +namespace: arc-systems diff --git a/clusters/unraid-lab/apps/arc-controller/application.yaml b/clusters/unraid-lab/apps/arc-controller/application.yaml deleted file mode 100644 index fb19869..0000000 --- a/clusters/unraid-lab/apps/arc-controller/application.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: arc-controller - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "1" -spec: - project: default - sources: - - repoURL: ghcr.io/actions/actions-runner-controller-charts - chart: gha-runner-scale-set-controller - targetRevision: "0.14.2" - helm: - releaseName: arc - valueFiles: - - $values/clusters/unraid-lab/apps/arc-controller/values.yaml - - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - ref: values - destination: - server: https://kubernetes.default.svc - namespace: arc-systems - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/clusters/unraid-lab/apps/harbor/app.yaml b/clusters/unraid-lab/apps/harbor/app.yaml new file mode 100644 index 0000000..1908c18 --- /dev/null +++ b/clusters/unraid-lab/apps/harbor/app.yaml @@ -0,0 +1,5 @@ +# unraid-lab-only chart-app. Base + cluster values layer. +chart: harbor +repoURL: https://helm.goharbor.io +version: "1.15.1" +namespace: harbor diff --git a/clusters/unraid-lab/apps/harbor/application.yaml b/clusters/unraid-lab/apps/harbor/application.yaml deleted file mode 100644 index 9634761..0000000 --- a/clusters/unraid-lab/apps/harbor/application.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: harbor - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "2" -spec: - project: default - sources: - - repoURL: https://helm.goharbor.io - chart: harbor - targetRevision: "1.15.1" # verify against a current Harbor 2.12.x chart - helm: - valueFiles: - - $values/clusters/unraid-lab/apps/harbor/values.yaml - - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - ref: values - destination: - server: https://kubernetes.default.svc # the unraid-lab cluster's own Argo CD - namespace: harbor - syncPolicy: - automated: - prune: false # registry data is precious; no auto-prune - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/clusters/unraid-lab/omni/inline-manifests.yaml b/clusters/unraid-lab/omni/inline-manifests.yaml index 651d11a..76e1d6f 100644 --- a/clusters/unraid-lab/omni/inline-manifests.yaml +++ b/clusters/unraid-lab/omni/inline-manifests.yaml @@ -1,35 +1,42 @@ # Talos inline manifests for the unraid-lab cluster bootstrap. # # Talos applies these on the first control-plane node before any scheduler is up. -# They install Argo CD; two seeded root Applications then fetch everything else -# from this repo (the "install Argo, then pull the rest remotely" pattern): +# They install Argo CD; three seeded roots then pull everything else from this +# repo ("install Argo, then fetch the rest remotely"): # -# root -> bootstrap/ shared infra (app-of-apps -> infrastructure/*) -# unraid-lab-apps -> clusters/unraid-lab/apps/ unraid-lab-only (Harbor, ARC runners) +# apps (ApplicationSet) -> apps/* + types/homelab-kvm/apps/* + clusters/unraid-lab/apps/* +# chart-apps, values layered base -> type -> cluster +# bootstrap (Application) -> bootstrap/ shared raw manifests (tailscale operator) +# unraid-raw (Application) -> clusters/unraid-lab/apps/*/application.yaml +# custom ApplicationSets that aren't chart-apps (arc-runners) # -# The OCI cluster uses ONE root (bootstrap/ only). unraid-lab needs BOTH, so its -# cluster-scoped apps under clusters/unraid-lab/ get seeded too — OCI never syncs -# that path, so those apps stay unraid-lab-only. +# The chart-app lane keys on app.yaml; the raw lane keys on application.yaml — so +# arc-runners (an ApplicationSet, application.yaml) and harbor/arc-controller +# (chart-apps, app.yaml) never double-manage. # -# Argo CD (and Cilium, IF this cluster is CNI-none like OCI — see the CNI decision -# in omni-on-unraid) are generated the same way OCI does it: -# the argocd block below is replaced by argo-cd manifests/install.yaml. +# Argo CD (and Cilium, IF unraid-lab is CNI-none like OCI — the CNI decision lives +# in omni-on-unraid) are filled by the generate step, same pattern as OCI. cluster: inlineManifests: - name: argocd contents: | - # !!! PLACEHOLDER — replace with the raw Argo CD install manifest !!! + # !!! PLACEHOLDER — raw Argo CD install manifest (namespace argocd) !!! # Source: curl -sL https://raw.githubusercontent.com/argoproj/argo-cd//manifests/install.yaml - # Namespace: argocd (create it in this manifest, as OCI's generated block does) - - name: argocd-root + - name: argocd-apps + contents: | + # Chart-app lane — inject repo-root appset.yaml with THIS cluster's identity: + # clusterName: unraid-lab, clusterType: homelab-kvm + # + # !!! generate step injects appset.yaml here !!! + + - name: argocd-bootstrap contents: | - # Shared infra — identical to what OCI runs. apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: root + name: bootstrap namespace: argocd spec: project: default @@ -45,14 +52,13 @@ cluster: prune: true selfHeal: true - - name: argocd-unraid-lab-apps + - name: argocd-unraid-raw contents: | - # unraid-lab-only apps (Harbor, ARC runners). Fans out to - # clusters/unraid-lab/apps/*/application.yaml. + # Raw lane for unraid-lab custom ApplicationSets (arc-runners). apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: unraid-lab-apps + name: unraid-raw namespace: argocd spec: project: default diff --git a/docs/architecture/gitops-layering.md b/docs/architecture/gitops-layering.md new file mode 100644 index 0000000..cf3efd2 --- /dev/null +++ b/docs/architecture/gitops-layering.md @@ -0,0 +1,83 @@ +# GitOps layering: base → type → cluster + +How apps and config are layered across clusters. Self-managed Argo CD (each +cluster runs its own), no central hub. + +## Bootstrap + +Talos `inlineManifests` (per cluster) install Argo CD, then seed **three roots**. +The only per-cluster difference is the cluster's identity (`clusterName`, +`clusterType`), baked into the seeded `appset`. + +- **`apps`** (ApplicationSet) — syncs `apps/*`, `types//apps/*`, + `clusters//apps/*`. The **chart-apps**, values layered. +- **`bootstrap`** (Application) — syncs `bootstrap/`. Shared raw manifests + (tailscale operator). +- **`-raw`** (Application) — syncs + `clusters//apps/*/application.yaml`. **Custom ApplicationSets** that + aren't chart-apps (arc-runners). + +Chart-app lane keys on **`app.yaml`**; raw lane keys on **`application.yaml`** — +so the two never double-manage an app in the same directory. + +## Two axes + +**Selection — which apps run where** (by directory): + +- `apps//app.yaml` — every cluster +- `types//apps//app.yaml` — every cluster of that type +- `clusters//apps//app.yaml` — one cluster + +**Values — override anything the chart exposes** (deep-merge `valueFiles`, later wins): + +```text +values/base/.yaml +values/types//.yaml # optional +values/clusters//.yaml # optional +``` + +Maps deep-merge; you write only the keys you change. (Lists replace, not merge — +Helm semantics.) + +## Overriding "anything" + +- **chart value** → add/patch a value file at the right layer. 99% of cases. +- **Argo `Application` field** (targetRevision, syncPolicy, …) → a kustomize + strategic-merge patch on the generated Application. +- **rendered chart internal not exposed as a value** → the rare escape hatch: a + kustomize post-render / SMP on the chart output. Brittle (pinned to the chart's + internal names) — use sparingly. + +## `app.yaml` shape + +```yaml +chart: harbor +repoURL: https://helm.goharbor.io # or an OCI registry for OCI charts +version: "1.15.1" +namespace: harbor +``` + +The `apps` ApplicationSet reads these + the cluster identity and templates one +Argo Application per app, with the layered `valueFiles`. + +## Adding / changing + +- **new app everywhere** → `apps//app.yaml` + `values/base/.yaml`. +- **app on one cluster** → `clusters//apps//app.yaml`. +- **override a value on one cluster** → `values/clusters//.yaml`. +- **custom ApplicationSet** (own generators, e.g. runners) → `application.yaml` + under `clusters//apps//` (raw lane). + +## Current state / TODO before merge + +- `appset.yaml` (repo root) is the canonical ApplicationSet; the generate step + injects it into each cluster's `inlineManifests` with that cluster's identity. + The `argocd` + `argocd-apps` inline blocks are still placeholders (filled by + `mise run :generate-manifests`) — wire the appset injection into that + script. +- **Not cluster-validated.** Needs a real sync on `unraid-lab` (once Argo CD is + bootstrapped there) + a no-op re-sync check on OCI before this replaces the + live setup. +- Migrated: cert-manager, cilium (shared, `apps/`); harbor, arc-controller + (unraid-only, `clusters/unraid-lab/apps/`). arc-runners stays a custom + ApplicationSet on the raw lane. diff --git a/docs/deploy/runbook.md b/docs/deploy/runbook.md new file mode 100644 index 0000000..c16da6d --- /dev/null +++ b/docs/deploy/runbook.md @@ -0,0 +1,85 @@ +# Deploy runbook + +Ordered steps to bring up the two clusters from this repo. Steps 1–2 and 6 are +in-repo (commit them); the rest run against real clusters and need secret +material you hold. Do not skip the pre-merge gate (step 5). + +Clusters: + +- `oci-talos` — type `cloud`, bootstraps its own Cilium (CNI-none Talos). +- `unraid-lab` — type `homelab-kvm`, no inline Cilium. + +## 1. Fill placeholders + +Edit `values/base/harbor.yaml`: replace both `harbor..ts.net` with the +cluster's real MagicDNS name. Nothing else in `values/` carries a placeholder. + +## 2. Generate inline-manifests, commit + +Fills each cluster's `argocd` (install), `argocd-apps` (appset with that +cluster's identity), and — OCI only — `cilium` block: + +```bash +mise run oci:generate-manifests +mise run unraid:generate-manifests +git add omni/patches/inline-manifests.yaml clusters/unraid-lab/omni/inline-manifests.yaml +git commit -m "chore: generate inline-manifests for deploy" +``` + +Version pins live in `omni/scripts/generate-manifests.sh` (`ARGOCD_VERSION`, +`CILIUM_VERSION`) — override via env to bump. + +## 3. Bootstrap the clusters (Omni) + +Apply the Talos machine config with the generated patch through Omni. On first +control-plane boot Talos applies the inline-manifests: Argo CD installs, then the +seeded root Applications pull the rest of this repo: + +- `argocd-apps` — the layering ApplicationSet (chart-apps, values `base → type → cluster`). +- `argocd-bootstrap` — syncs `bootstrap/` (Tailscale operator). +- `argocd-unraid-raw` (unraid only) — syncs `clusters/unraid-lab/apps/*/application.yaml` (arc-runners). + +## 4. Create in-cluster secrets + +Apps stay `Progressing` until these exist. `harbor-mirror-robot` depends on +Harbor being up (step 7) — create it there, not here. + +- `harbor-admin-password` — ns `harbor`, key `HARBOR_ADMIN_PASSWORD`. +- `arc-gha-secret` — ns `arc-runners`, the GitHub App credentials + (`github_app_id`, `github_app_installation_id`, `github_app_private_key`). + +Generate the App private key yourself; never commit any of these. + +## 5. Pre-merge gate + +Do not merge #17 until both pass: + +- **unraid-lab**: every Argo Application `Synced` + `Healthy` + (`argocd app list`), Harbor and arc-controller included. +- **OCI**: re-sync is a **no-op** — the restructure must not churn a running + cluster. `argocd app diff` shows no changes. + +## 6. Merge #17 + +The layering ApplicationSet, `validate.py`, and the generate tooling land on +`main` together once the gate is green. + +## 7. Harbor post-deploy bootstrap + +With Harbor reachable at its MagicDNS name: + +- Create project `image-factory`. +- Create the push robot (Image Factory) and the mirror robot; store the mirror + robot creds as secret `harbor-mirror-robot` (keys `username`, `password`) in + ns `arc-runners` — this is what step 4 deferred. +- Wire OIDC (PocketID) per the image-factory-registry plan. + +Detail: `syscode-ai-internal-plans/projects/image-factory-registry/plans/2026-07-19-harbor-ghcr-mirror.md`. + +## 8. Repoint Image Factory, revoke the PAT + +Apply the omni-on-unraid reconfig (points Image Factory at Harbor, drops the +`GITHUB_TOKEN`), confirm end-to-end, then revoke the GHCR PAT — last, only after +sign-off. + +Detail: `syscode-ai-internal-plans/projects/image-factory-registry/handoffs/2026-07-20-omni-image-factory-reconfig.md`. diff --git a/infrastructure/cert-manager/application.yaml b/infrastructure/cert-manager/application.yaml deleted file mode 100644 index bcda789..0000000 --- a/infrastructure/cert-manager/application.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: cert-manager - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "2" -spec: - project: default - sources: - - repoURL: https://charts.jetstack.io - chart: cert-manager - targetRevision: "v1.17.1" - helm: - valueFiles: - - $values/infrastructure/cert-manager/values.yaml - - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - ref: values - destination: - server: https://kubernetes.default.svc - namespace: cert-manager - syncPolicy: - automated: - prune: false - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true diff --git a/infrastructure/cilium/application.yaml b/infrastructure/cilium/application.yaml deleted file mode 100644 index 9234216..0000000 --- a/infrastructure/cilium/application.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: cilium - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "1" -spec: - project: default - sources: - - repoURL: https://helm.cilium.io - chart: cilium - targetRevision: "1.17.1" - helm: - valueFiles: - - $values/infrastructure/cilium/values.yaml - - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git - targetRevision: HEAD - ref: values - destination: - server: https://kubernetes.default.svc - namespace: kube-system - syncPolicy: - automated: - prune: false - selfHeal: true - syncOptions: - - CreateNamespace=false - - ServerSideApply=true diff --git a/omni/patches/inline-manifests.yaml b/omni/patches/inline-manifests.yaml index e0a8f6e..2dc0b01 100644 --- a/omni/patches/inline-manifests.yaml +++ b/omni/patches/inline-manifests.yaml @@ -41,14 +41,24 @@ cluster: # # !!! PLACEHOLDER — run mise run oci:generate-manifests to populate !!! - - name: argocd-app-of-apps + - name: argocd-apps contents: | - # Inline root Application. It syncs bootstrap/, which then fans out to infrastructure/. - # Update REPO_URL and TARGET_REVISION to match your apps repo. + # Chart-app lane — the layering ApplicationSet. The generate step injects + # the repo-root appset.yaml with THIS cluster's identity: + # clusterName: oci-talos, clusterType: cloud + # It deploys apps/* (shared) + types/cloud/apps/* + clusters/oci-talos/apps/* + # with values layered base -> type -> cluster. + # + # !!! run: mise run oci:generate-manifests — injects appset.yaml here !!! + + - name: argocd-bootstrap + contents: | + # Raw-manifest lane: syncs bootstrap/ (shared non-chart manifests, e.g. + # the Tailscale operator). apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: root + name: bootstrap namespace: argocd spec: project: default diff --git a/omni/scripts/generate-manifests.sh b/omni/scripts/generate-manifests.sh index 1f7740f..1be44fa 100755 --- a/omni/scripts/generate-manifests.sh +++ b/omni/scripts/generate-manifests.sh @@ -1,85 +1,95 @@ #!/usr/bin/env bash -# Generate inline manifest content for omni/patches/inline-manifests.yaml. -# Run this after version bumps for Cilium or Argo CD, then commit the result. +# Fill the generated blocks of a cluster's Talos inline-manifests: +# argocd raw Argo CD install manifest (upstream install.yaml) +# argocd-apps the repo-root appset.yaml with THIS cluster's identity substituted +# cilium rendered Cilium (only clusters that bootstrap their own CNI, e.g. OCI) # -# Requirements: helm, curl, yq (or python3) +# The static blocks (argocd-bootstrap, argocd-unraid-raw) and every comment are +# left untouched — yq edits only the named blocks in place. +# +# Usage: omni/scripts/generate-manifests.sh +# or: mise run oci:generate-manifests / mise run unraid:generate-manifests +# +# Requirements: yq (mikefarah v4), curl, helm, kubectl. Run after Cilium/Argo CD version +# bumps or appset.yaml changes, then review + commit the result. set -euo pipefail +CLUSTER="${1:?usage: generate-manifests.sh }" CILIUM_VERSION="${CILIUM_VERSION:-1.17.2}" ARGOCD_VERSION="${ARGOCD_VERSION:-v2.14.4}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PATCH_FILE="${SCRIPT_DIR}/../patches/inline-manifests.yaml" -TMPDIR=$(mktemp -d) -trap 'rm -rf "$TMPDIR"' EXIT - -echo "==> Adding Cilium Helm repo..." -helm repo add cilium https://helm.cilium.io/ --force-update 2>/dev/null -helm repo update cilium +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -echo "==> Rendering Cilium ${CILIUM_VERSION}..." -helm template cilium cilium/cilium \ - --version "${CILIUM_VERSION}" \ - --namespace kube-system \ - --set kubeProxyReplacement=true \ - --set k8sServiceHost=localhost \ - --set k8sServicePort=7445 \ - --set ipam.mode=kubernetes \ - --set hubble.relay.enabled=true \ - --set hubble.ui.enabled=true \ - > "${TMPDIR}/cilium.yaml" +case "$CLUSTER" in + oci-talos) + CLUSTER_TYPE="cloud" + FILE="omni/patches/inline-manifests.yaml" + WITH_CILIUM=1 + ;; + unraid-lab) + CLUSTER_TYPE="homelab-kvm" + FILE="clusters/unraid-lab/omni/inline-manifests.yaml" + WITH_CILIUM=0 + ;; + *) + echo "unknown cluster '$CLUSTER' (expected oci-talos or unraid-lab)" >&2 + exit 1 + ;; +esac +FILE="$REPO_ROOT/$FILE" -echo "==> Downloading Argo CD ${ARGOCD_VERSION}..." -curl -sL "https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml" \ - > "${TMPDIR}/argocd.yaml" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT -CILIUM_CONTENT=$(sed 's/^/ /' "${TMPDIR}/cilium.yaml") -ARGOCD_CONTENT=$(sed 's/^/ /' "${TMPDIR}/argocd.yaml") - -echo "==> Writing ${PATCH_FILE}..." -cat > "${PATCH_FILE}" < Argo CD ${ARGOCD_VERSION} install manifest (namespaced to argocd)..." +# Upstream install.yaml carries NO namespace on its resources — it relies on +# `kubectl apply -n argocd`. Talos applies inline manifests verbatim with no +# namespace default, so the namespaced resources would miss the argocd namespace +# and never install. Stamp it with kustomize (also fixes the RBAC binding +# subjects), and prepend the Namespace since kustomize won't create it. +mkdir -p "$TMP/argocd" +curl -sfL "https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml" \ + -o "$TMP/argocd/install.yaml" +cat > "$TMP/argocd/kustomization.yaml" <<'KUST' +namespace: argocd +resources: + - install.yaml +KUST +{ printf 'apiVersion: v1\nkind: Namespace\nmetadata:\n name: argocd\n---\n'; \ + kubectl kustomize "$TMP/argocd"; } > "$TMP/argocd-manifest.yaml" +inject argocd "$TMP/argocd-manifest.yaml" - - name: argocd - contents: | - # argocd ${ARGOCD_VERSION} — generated $(date -u +%Y-%m-%d) -${ARGOCD_CONTENT} +echo "==> appset.yaml for ${CLUSTER} / ${CLUSTER_TYPE}..." +sed -e "s/CLUSTER_NAME/${CLUSTER}/g" -e "s/CLUSTER_TYPE/${CLUSTER_TYPE}/g" \ + "$REPO_ROOT/appset.yaml" > "$TMP/appset.yaml" +inject argocd-apps "$TMP/appset.yaml" - - name: argocd-app-of-apps - contents: | - # Inline root Application. It syncs bootstrap/, which then fans out to infrastructure/. - apiVersion: argoproj.io/v1alpha1 - kind: Application - metadata: - name: root - namespace: argocd - spec: - project: default - source: - repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps - targetRevision: HEAD - path: bootstrap - destination: - server: https://kubernetes.default.svc - namespace: argocd - syncPolicy: - automated: - prune: true - selfHeal: true -EOF +if [[ "$WITH_CILIUM" == 1 ]]; then + echo "==> Cilium ${CILIUM_VERSION} (kube-proxy-free, KubePrism)..." + helm repo add cilium https://helm.cilium.io/ --force-update >/dev/null 2>&1 + helm repo update cilium >/dev/null + helm template cilium cilium/cilium \ + --version "${CILIUM_VERSION}" \ + --namespace kube-system \ + --set kubeProxyReplacement=true \ + --set k8sServiceHost=localhost \ + --set k8sServicePort=7445 \ + --set ipam.mode=kubernetes \ + --set hubble.relay.enabled=true \ + --set hubble.ui.enabled=true \ + > "$TMP/cilium.yaml" + inject cilium "$TMP/cilium.yaml" +fi -echo "==> Done. Commit omni/patches/inline-manifests.yaml." +echo "==> Wrote ${FILE#"$REPO_ROOT"/} (${CLUSTER} / ${CLUSTER_TYPE}). Review + commit." diff --git a/omni/scripts/inject-secrets.sh b/omni/scripts/inject-secrets.sh new file mode 100755 index 0000000..3ec68f2 --- /dev/null +++ b/omni/scripts/inject-secrets.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Deliver unraid-lab bootstrap secrets through Omni — NOT through git. +# +# Reads values from a gitignored file (omni/secrets.env), builds the k8s Secret +# manifests each app expects, wraps them in a Talos machine-config patch, and +# applies it to Omni as a ConfigPatch for the unraid-lab cluster. The secrets then +# live only in Omni's encrypted config store and in the cluster — never committed. +# +# Usage: +# omni/scripts/inject-secrets.sh # dry-run: print the ConfigPatch, validate +# omni/scripts/inject-secrets.sh --apply # push to Omni (needs omnictl auth) +# +# Requirements: yq (mikefarah v4), omnictl (with OMNICONFIG). Fill omni/secrets.env +# from omni/secrets.env.example first. harbor-mirror-robot is intentionally NOT here +# — it's a Harbor robot account, created after Harbor is running (add it later). +set -euo pipefail + +CLUSTER="unraid-lab" +YQ="${YQ:-yq}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ENV_FILE="${SECRETS_ENV:-$REPO_ROOT/omni/secrets.env}" + +[ -f "$ENV_FILE" ] || { echo "missing $ENV_FILE — copy omni/secrets.env.example and fill it" >&2; exit 1; } +set -a; . "$ENV_FILE"; set +a + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +# k8s manifests (namespaces are idempotent; keeps a fresh bootstrap self-contained). +cat > "$TMP/manifests.yaml" < Talos inline-manifest patch -> Omni ConfigPatch (yq handles the +# nested block scalars, so no hand-indentation). +printf 'cluster:\n inlineManifests: []\n' > "$TMP/patch.yaml" +MF="$TMP/manifests.yaml" "$YQ" -i \ + '.cluster.inlineManifests += [{"name":"bootstrap-secrets","contents": loadstr(strenv(MF))}] | + (.cluster.inlineManifests[0].contents) style="literal"' "$TMP/patch.yaml" + +cat > "$TMP/configpatch.yaml" < applied ConfigPatch 500-cluster-${CLUSTER}-bootstrap-secrets (secrets now in Omni, delivered to cluster)" +else + echo "----- ConfigPatch (dry-run; secret values redacted below) -----" + sed -E 's/(HARBOR_ADMIN_PASSWORD|client_secret|github_app_private_key).*/\1: /' "$TMP/configpatch.yaml" + echo "----- re-run with --apply to push to Omni -----" +fi diff --git a/omni/secrets.env.example b/omni/secrets.env.example new file mode 100644 index 0000000..a8ad550 --- /dev/null +++ b/omni/secrets.env.example @@ -0,0 +1,10 @@ +# Copy to omni/secrets.env (gitignored) and fill. Delivered to unraid-lab via Omni +# (omni/scripts/inject-secrets.sh --apply) — never committed. +HARBOR_ADMIN_PASSWORD= # choose a strong password +GITHUB_APP_ID= # syscode-labs-arc-runners App id +GITHUB_APP_INSTALLATION_ID= # its installation id +# The App private key PEM (multi-line). Easiest: point it at the downloaded file: +# GITHUB_APP_PRIVATE_KEY="$(cat ~/syscode-labs-arc-runners.private-key.pem)" +GITHUB_APP_PRIVATE_KEY= +TS_OAUTH_CLIENT_ID= # Tailscale OAuth client id (tag:k8s-operator) +TS_OAUTH_CLIENT_SECRET= # Tailscale OAuth client secret diff --git a/scripts/validate.py b/scripts/validate.py index c627040..6b12909 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -107,8 +107,25 @@ def main() -> int: if docs is None: ok = False continue + + # chart-app coord file (apps/*/app.yaml) consumed by appset.yaml. No kind; + # render it the way the ApplicationSet would — chart + base values. + # ponytail: base layer only; values/types|clusters overlays are per-cluster + # (empty for now) and validated at sync time, not repo-wide here. + if os.path.basename(path) == "app.yaml" and docs and "chart" in docs[0]: + coord = docs[0] + app = os.path.basename(os.path.dirname(path)) + base_values = f"values/base/{app}.yaml" + value_files = [base_values] if os.path.exists(base_values) else [] + print(f"── render chart-app {app} ({path})") + if not render_and_check( + app, coord["repoURL"], coord["chart"], str(coord.get("version", "")), value_files + ): + ok = False + continue + if not any("kind" in d for d in docs): - continue # values / Talos patch / coord file — not a k8s manifest + continue # values / Talos patch — not a k8s manifest for doc in docs: if doc.get("kind") == "Application": diff --git a/types/homelab-kvm/apps/csi-driver-nfs/app.yaml b/types/homelab-kvm/apps/csi-driver-nfs/app.yaml new file mode 100644 index 0000000..f22fa9b --- /dev/null +++ b/types/homelab-kvm/apps/csi-driver-nfs/app.yaml @@ -0,0 +1,9 @@ +# App coords consumed by the layering ApplicationSet. Storage for homelab-kvm +# (unraid-lab): PVs backed by an unraid NFS export (data on the parity-protected +# array — unraid IS the data-layer replication). Cloud clusters use their provider +# CSI instead (per-type storage). NFS client is in-kernel, so unlike Longhorn this +# needs NO Talos image extension. +chart: csi-driver-nfs +repoURL: https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts +version: "4.13.4" +namespace: kube-system diff --git a/clusters/unraid-lab/apps/arc-controller/values.yaml b/values/base/arc-controller.yaml similarity index 100% rename from clusters/unraid-lab/apps/arc-controller/values.yaml rename to values/base/arc-controller.yaml diff --git a/infrastructure/cert-manager/values.yaml b/values/base/cert-manager.yaml similarity index 100% rename from infrastructure/cert-manager/values.yaml rename to values/base/cert-manager.yaml diff --git a/infrastructure/cilium/values.yaml b/values/base/cilium.yaml similarity index 100% rename from infrastructure/cilium/values.yaml rename to values/base/cilium.yaml diff --git a/values/base/csi-driver-nfs.yaml b/values/base/csi-driver-nfs.yaml new file mode 100644 index 0000000..ff938a6 --- /dev/null +++ b/values/base/csi-driver-nfs.yaml @@ -0,0 +1,18 @@ +# csi-driver-nfs — default StorageClass for homelab-kvm (unraid-lab). +# PVs live on the unraid NFS export /mnt/user/unraid-talos-pv-store (parity-protected +# array). server is the unraid LAN IP — nfsd does not listen on the libvirt virbr0 +# side, and the NAT'd nodes reach it there. The export uses no_root_squash so the +# provisioner can create and own per-PV subdirectories. Default class so Harbor's +# PVCs bind with no per-app change. +storageClass: + create: true + name: nfs + annotations: + storageclass.kubernetes.io/is-default-class: "true" + parameters: + server: 10.10.210.59 + share: /mnt/user/unraid-talos-pv-store + reclaimPolicy: Retain # registry data — keep backing dir on PVC delete + volumeBindingMode: Immediate + mountOptions: + - nfsvers=4.1 diff --git a/clusters/unraid-lab/apps/harbor/values.yaml b/values/base/harbor.yaml similarity index 100% rename from clusters/unraid-lab/apps/harbor/values.yaml rename to values/base/harbor.yaml diff --git a/values/clusters/oci-talos/.gitkeep b/values/clusters/oci-talos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/values/clusters/unraid-lab/.gitkeep b/values/clusters/unraid-lab/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/values/types/cloud/.gitkeep b/values/types/cloud/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/values/types/homelab-kvm/.gitkeep b/values/types/homelab-kvm/.gitkeep new file mode 100644 index 0000000..e69de29