diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..95e09e2 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Phase-0 bootstrap — bring a bare Talos cluster to a self-hosted GitOps control +# plane, breaking the ArgoCD <-> Forgejo circle via a GitHub seed + cutover. +# See docs/adr/0001-gitops-bootstrap-and-cd.md (Part A) and docs/plans/0001-EXECUTION.md. +# +# Order (all manual, once): Cilium -> Longhorn -> CNPG operator -> forgejo-db +# (wait Ready) -> Forgejo -> ArgoCD (seeded from GitHub) -> cutover to Forgejo. +# Everything ELSE is deployed by ArgoCD from the seed repo, in sync-wave order. +# +# Prereqs: +# - Talos cluster up; kubectl context points at it +# - helm 3, kubectl +# - SOPS age key at $SOPS_KEY (for the ArgoCD SOPS CMP plugin) +# - GitHub read-only deploy key private half at $DEPLOY_KEY (public half added +# to the GitHub repo's Deploy keys) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BOOT="$SCRIPT_DIR/k8s/bootstrap" +SOPS_KEY="${SOPS_KEY:-$HOME/.sops/homelab-age.key}" +DEPLOY_KEY="${DEPLOY_KEY:-$HOME/.ssh/argocd_seed}" +GITHUB_SSH="git@github.com:Riotpiaole/riotpiao.homelab.com.git" + +log() { echo "[$(date +%H:%M:%S)] $*"; } +die() { echo "ERROR: $*" >&2; exit 1; } +phase(){ echo; echo "━━━ $* ━━━"; echo; } + +preflight() { + log "preflight…" + kubectl cluster-info >/dev/null || die "kubectl not configured / cluster unreachable" + command -v helm >/dev/null || die "helm 3 not found" + [[ -f "$SOPS_KEY" ]] || die "SOPS age key missing at $SOPS_KEY" + [[ -f "$DEPLOY_KEY" ]] || die "GitHub deploy key missing at $DEPLOY_KEY (see phase4-argocd/seed-repo-secret.example.yaml)" + log "✅ preflight ok" +} + +p1_cilium() { + phase "PHASE 1a: CNI (Cilium)" + if kubectl -n kube-system get ds cilium >/dev/null 2>&1; then log "cilium present, skip"; return; fi + helm repo add cilium https://helm.cilium.io >/dev/null; helm repo update >/dev/null + helm install cilium cilium/cilium -n kube-system \ + --set ipam.mode=kubernetes --set kubeProxyReplacement=true --wait --timeout 10m + log "✅ cilium installed" +} + +p1_longhorn() { + phase "PHASE 1b: STORAGE (Longhorn)" + if helm -n longhorn-system list 2>/dev/null | grep -q longhorn; then log "longhorn present, skip"; return; fi + helm repo add longhorn https://charts.longhorn.io >/dev/null; helm repo update >/dev/null + helm install longhorn longhorn/longhorn -n longhorn-system --create-namespace \ + --values "$BOOT/phase1-storage/longhorn-values.yaml" --wait --timeout 10m + kubectl -n longhorn-system wait --for=condition=available --timeout=300s deploy/longhorn-manager + kubectl apply -f "$BOOT/phase1-storage/storageclasses.yaml" + log "✅ longhorn installed" +} + +p2_cnpg() { + phase "PHASE 2: CNPG OPERATOR" + if helm -n cnpg-system list 2>/dev/null | grep -q cnpg; then log "cnpg present, skip"; return; fi + helm repo add cnpg https://cloudnative-pg.github.io/charts >/dev/null; helm repo update >/dev/null + helm install cnpg cnpg/cloudnative-pg -n cnpg-system --create-namespace \ + --values "$BOOT/phase2-cnpg/cnpg-values.yaml" --wait --timeout 5m + kubectl get crd clusters.postgresql.cnpg.io >/dev/null || die "CNPG CRD not registered" + log "✅ cnpg operator installed" +} + +p3_forgejo() { + phase "PHASE 3: forgejo-db + Forgejo (ns cicd)" + kubectl create ns cicd --dry-run=client -o yaml | kubectl apply -f - + kubectl apply -f "$BOOT/phase3-forgejo/forgejo-db.yaml" + log "waiting for forgejo-db Ready (3-5 min)…" + kubectl wait --for=condition=Ready --timeout=600s cluster/forgejo-db -n cicd + kubectl -n cicd get secret forgejo-db-app >/dev/null || die "CNPG did not create forgejo-db-app secret" + if helm -n cicd list 2>/dev/null | grep -q forgejo; then log "forgejo present, skip"; return; fi + helm repo add forgejo https://code.forgejo.org/forgejo-helm >/dev/null 2>&1 || \ + helm repo add forgejo https://dl.gitea.io/charts/ >/dev/null + helm repo update >/dev/null + helm install forgejo forgejo/forgejo -n cicd \ + --values "$BOOT/phase3-forgejo/forgejo-values.yaml" --wait --timeout 10m + log "✅ forgejo up — now push this repo to Forgejo and configure the GitHub pull-mirror" +} + +p4_argocd() { + phase "PHASE 4: ArgoCD (seeded from GitHub)" + kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f - + # SOPS age key for the repo-server CMP plugin + kubectl -n argocd create secret generic sops-age \ + --from-file=keys.txt="$SOPS_KEY" --dry-run=client -o yaml | kubectl apply -f - + # GitHub deploy-key repo credential (read-only) + kubectl -n argocd create secret generic seed-github-repo \ + --from-literal=type=git --from-literal=url="$GITHUB_SSH" \ + --from-file=sshPrivateKey="$DEPLOY_KEY" --dry-run=client -o yaml | kubectl apply -f - + kubectl -n argocd label secret seed-github-repo argocd.argoproj.io/secret-type=repository --overwrite + if ! helm -n argocd list 2>/dev/null | grep -q argocd; then + helm repo add argo https://argoproj.github.io/argo-helm >/dev/null; helm repo update >/dev/null + helm install argocd argo/argo-cd -n argocd \ + --values "$BOOT/phase4-argocd/argocd-values.yaml" --wait --timeout 10m + fi + kubectl -n argocd wait --for=condition=available --timeout=300s deploy/argocd-server + kubectl apply -f "$BOOT/phase4-argocd/root-app-github.yaml" + log "✅ ArgoCD syncing from GitHub seed. Watch: kubectl get applications -n argocd" +} + +p5_cutover() { + phase "PHASE 5: CUTOVER GitHub -> Forgejo" + read -rp "Forgejo healthy AND mirroring GitHub? (y/N) " r; [[ $r =~ ^[Yy]$ ]] || die "push+mirror to Forgejo first" + kubectl apply -f "$BOOT/phase5-cutover/root-app-forgejo.yaml" + log "✅ root app now sourced from Forgejo. GitHub mirror = DR seed. Circle dead." +} + +case "${1:-all}" in + all) preflight; p1_cilium; p1_longhorn; p2_cnpg; p3_forgejo; p4_argocd + log "Phases 1-4 done. Push repo to Forgejo + set up pull-mirror, then: $0 cutover" ;; + cilium) preflight; p1_cilium ;; + storage) preflight; p1_longhorn ;; + cnpg) preflight; p2_cnpg ;; + forgejo) preflight; p3_forgejo ;; + argocd) preflight; p4_argocd ;; + cutover) preflight; p5_cutover ;; + *) echo "usage: $0 {all|cilium|storage|cnpg|forgejo|argocd|cutover}"; exit 1 ;; +esac diff --git a/k8s/bootstrap/phase4-argocd/root-app-github.yaml b/k8s/bootstrap/phase4-argocd/root-app-github.yaml index 61f40fd..f574a4f 100644 --- a/k8s/bootstrap/phase4-argocd/root-app-github.yaml +++ b/k8s/bootstrap/phase4-argocd/root-app-github.yaml @@ -1,8 +1,10 @@ -# ArgoCD Root App-of-Apps — GitHub Mirror Source -# This is the initial configuration that breaks the circular dependency -# Points at GitHub mirror, not Forgejo (Forgejo isn't ready yet) -# -# After Forgejo is healthy and repo is pushed, use phase5-cutover/root-app-forgejo.yaml +# ArgoCD Root App-of-Apps — GitHub seed source (circle-breaker). +# Applied ONCE at Phase 0 (manual), BEFORE Forgejo serves the repo. Points at the +# GitHub seed so ArgoCD can deploy everything after the control plane. After +# Forgejo is healthy + mirroring GitHub, cut over with phase5-cutover/root-app-forgejo.yaml. +# +# repoURL is the SSH form — must match the `url` in the seed-repo deploy-key Secret +# (see seed-repo-secret.example.yaml). Apply that Secret before this. --- apiVersion: v1 kind: Namespace @@ -16,11 +18,12 @@ metadata: namespace: argocd spec: description: Homelab infrastructure and applications + # Single-tenant homelab: all sources are trusted (GitHub seed, Forgejo, and the + # public Helm chart repos the app tree pulls from). Wildcard avoids silently + # blocking a chart host (operator.min.io, hashicorp, strimzi.io, go.temporal.io, + # dl.gitea.io, *.github.io, charts.*, …). sourceRepos: - - 'https://github.com/YOUR-ORG/homelab.git' # ← REPLACE with your GitHub mirror - - 'https://forgejo.riotpiao.com/YOUR-ORG/homelab.git' - - 'https://*.github.io/*' # Helm charts from GitHub Pages - - 'https://charts.*' # Public Helm repos + - '*' destinations: - namespace: '*' server: 'https://kubernetes.default.svc' @@ -40,16 +43,13 @@ metadata: - resources-finalizer.argocd.argoproj.io spec: project: homelab - source: - repoURL: https://github.com/YOUR-ORG/homelab.git # ← REPLACE with your GitHub mirror + repoURL: git@github.com:Riotpiaole/riotpiao.homelab.com.git # GitHub seed (SSH) targetRevision: main path: k8s/argocd/apps - destination: server: https://kubernetes.default.svc namespace: argocd - syncPolicy: automated: prune: true diff --git a/k8s/bootstrap/phase4-argocd/seed-repo-secret.example.yaml b/k8s/bootstrap/phase4-argocd/seed-repo-secret.example.yaml new file mode 100644 index 0000000..d71d3d9 --- /dev/null +++ b/k8s/bootstrap/phase4-argocd/seed-repo-secret.example.yaml @@ -0,0 +1,28 @@ +# ArgoCD repo credential for the PRIVATE GitHub seed — deploy key (read-only). +# Apply at Phase 0 BEFORE root-app-github.yaml. This is a TEMPLATE: never commit +# the real private key. +# +# ssh-keygen -t ed25519 -C "argocd@homelab" -f argocd_seed -N "" +# # add argocd_seed.pub → GitHub repo → Settings → Deploy keys (Read-only, no write) +# kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f - +# kubectl -n argocd create secret generic seed-github-repo \ +# --from-literal=type=git \ +# --from-literal=url=git@github.com:Riotpiaole/riotpiao.homelab.com.git \ +# --from-file=sshPrivateKey=argocd_seed +# kubectl -n argocd label secret seed-github-repo argocd.argoproj.io/secret-type=repository +# +# url MUST match root-app-github.yaml's repoURL (SSH form). +apiVersion: v1 +kind: Secret +metadata: + name: seed-github-repo + namespace: argocd + labels: + argocd.argoproj.io/secret-type: repository +stringData: + type: git + url: git@github.com:Riotpiaole/riotpiao.homelab.com.git + sshPrivateKey: | + -----BEGIN OPENSSH PRIVATE KEY----- + REPLACE-WITH-READ-ONLY-DEPLOY-KEY-PRIVATE-HALF — DO NOT COMMIT THE REAL KEY + -----END OPENSSH PRIVATE KEY----- diff --git a/k8s/bootstrap/phase5-cutover/root-app-forgejo.yaml b/k8s/bootstrap/phase5-cutover/root-app-forgejo.yaml index c6ba0a1..2badfee 100644 --- a/k8s/bootstrap/phase5-cutover/root-app-forgejo.yaml +++ b/k8s/bootstrap/phase5-cutover/root-app-forgejo.yaml @@ -15,7 +15,7 @@ spec: project: homelab source: - repoURL: https://forgejo.riotpiao.com/YOUR-ORG/homelab.git # ← REPLACE with your Forgejo URL + repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git # Forgejo (post-cutover source of truth) targetRevision: main path: k8s/argocd/apps