diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..140813c --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,95 @@ +name: Validate and Deploy to K3s + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl + run: | + if ! command -v kubectl &> /dev/null; then + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" + elif [ "$ARCH" = "x86_64" ]; then + ARCH="amd64" + fi + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + fi + kubectl version --client + + - name: Configure kubectl + run: | + mkdir -p $HOME/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config + chmod 600 $HOME/.kube/config + + - name: Server-side dry-run of full kustomization + run: | + kubectl apply -k . --dry-run=server + + deploy: + needs: validate + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install kubectl + run: | + if ! command -v kubectl &> /dev/null; then + ARCH=$(uname -m) + if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" + elif [ "$ARCH" = "x86_64" ]; then + ARCH="amd64" + fi + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + fi + kubectl version --client + + - name: Configure kubectl + run: | + mkdir -p $HOME/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config + chmod 600 $HOME/.kube/config + + - name: Apply kustomization + run: | + kubectl apply -k . + + - name: Wait for rollouts + run: | + set -e + kubectl rollout status deploy/bitwarden-vaultwarden -n bitwarden --timeout=10m + kubectl rollout status deploy/searxng-1723974683 -n default --timeout=10m + kubectl rollout status deploy/home-assistant -n home-assistant --timeout=15m + kubectl rollout status deploy/nginx -n nginx --timeout=10m + kubectl rollout status deploy/pihole -n pihole --timeout=10m + kubectl rollout status deploy/unbound -n pihole --timeout=10m + kubectl rollout status deploy/unifiedstreaming -n unified-streaming --timeout=10m + + - name: Deployment summary + if: always() + run: | + echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + for ns in bitwarden default home-assistant nginx pihole unified-streaming; do + kubectl get deployments -n $ns >> $GITHUB_STEP_SUMMARY || true + done + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index e702956..5f979e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Real secrets never go in git — only *.example.yaml templates do. apps/**/secret.yaml *.secret.yaml +.deploy-kubeconfig.b64 diff --git a/README.md b/README.md index 6c4098d..1c32e69 100644 --- a/README.md +++ b/README.md @@ -43,21 +43,26 @@ Three apps read secrets that are NOT in git. Each app dir has a | `usp-license` | unified-streaming | `USP_LICENSE_KEY` | | `searxng-1723974683-config` | default | searxng `settings.yml` | -**One-time migration (pihole + unified-streaming):** the live cluster (as of -2026-07-26) has WEBPASSWORD / USP_LICENSE_KEY as inline plaintext `value`s. -`kubectl apply` (client- or server-side) cannot patch an env var from `value` -to `valueFrom` — the API rejects the merged object. Validated procedure: +~~**One-time migration (pihole + unified-streaming)**~~ — DONE 2026-07-26: +secrets `pihole-admin` and `usp-license` created in-cluster and both +deployments switched to `secretKeyRef` via `kubectl replace` (a plain +`kubectl apply` cannot patch an env var from `value` to `valueFrom`). +`kubectl apply -k .` now converges cleanly (verified `--dry-run=server`). -```sh -# 1. create the two secrets (see secret.example.yaml in each app dir) -# 2. one-time replace (restarts the pods): -kubectl replace -f apps/pihole/deployment-pihole.yaml -kubectl replace -f apps/unified-streaming/deployment.yaml -``` +## CI/CD (Gitea Actions) -After that, `kubectl apply -k .` works cleanly for everything (verified with -`--dry-run=server` against the live cluster, 2026-07-26 — zero immutable-field -conflicts). +`.gitea/workflows/deploy.yaml`, modeled on socktop-webterm's pipeline: + +- **PRs** → `kubectl apply -k . --dry-run=server` (validation only) +- **push to main** → `kubectl apply -k .` + `kubectl rollout status` on all + seven deployments + +Repo Actions secret required: `KUBECONFIG` — base64-encoded kubeconfig for the +`gitea-deployer` ServiceAccount (same identity socktop-webterm deploys with; +regenerate anytime from `default/gitea-deployer-token`). Its RBAC is +`rbac/gitea-deployer.yaml` — admin-applied once, deliberately outside the root +kustomization so the pipeline token cannot escalate itself; it has no access +to secrets and no delete verbs. ## Cluster assumptions (state that lives outside these manifests) diff --git a/rbac/gitea-deployer.yaml b/rbac/gitea-deployer.yaml new file mode 100644 index 0000000..0e5b3fb --- /dev/null +++ b/rbac/gitea-deployer.yaml @@ -0,0 +1,51 @@ +# RBAC for the Gitea Actions deploy pipeline. +# +# ADMIN BOOTSTRAP — applied once by a human with cluster-admin +# (NOT part of the root kustomization: the pipeline's own token must not be +# able to escalate its privileges): +# kubectl apply -f rbac/gitea-deployer.yaml +# +# Binds to the existing `gitea-deployer` ServiceAccount in `default` +# (token secret: default/gitea-deployer-token — the same identity the +# socktop-webterm pipeline uses). +# +# Deliberately granted: the resource kinds this repo contains, cluster-wide. +# Deliberately NOT granted: secrets (app secrets are created out-of-band), +# RBAC objects, nodes, or delete on anything. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: homelab-k3s-deployer +rules: + - apiGroups: [""] + resources: + - namespaces + - persistentvolumes + - persistentvolumeclaims + - services + verbs: [get, list, watch, create, update, patch] + - apiGroups: [apps] + resources: [deployments] + verbs: [get, list, watch, create, update, patch] + - apiGroups: [networking.k8s.io] + resources: [ingresses] + verbs: [get, list, watch, create, update, patch] + - apiGroups: [apps] + resources: [replicasets] + verbs: [get, list, watch] + - apiGroups: [""] + resources: [pods, events] + verbs: [get, list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: homelab-k3s-deployer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: homelab-k3s-deployer +subjects: + - kind: ServiceAccount + name: gitea-deployer + namespace: default