#!/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