Compare commits

..

No commits in common. "fe72f1e85a3fc7096d90a785c60d45a893b7cbc3" and "8afac39d0350c08b36019a39f7e278a94c507476" have entirely different histories.

5 changed files with 21 additions and 194 deletions

View File

@ -1,95 +0,0 @@
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

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
# Real secrets never go in git — only *.example.yaml templates do.
apps/**/secret.yaml
*.secret.yaml
.deploy-kubeconfig.b64

View File

@ -43,26 +43,21 @@ 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)**~~ — 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`).
**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:
## CI/CD (Gitea Actions)
```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
```
`.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.
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).
## Cluster assumptions (state that lives outside these manifests)
@ -85,10 +80,7 @@ to secrets and no delete verbs.
## Known quirks (faithful to live state, fix at leisure)
- Most images are `:latest`/`:stable` — deploys are not reproducible until
pinned. vaultwarden is pinned (1.35.4); pihole is pinned by digest (Core
v6.2.2) after `:latest` silently jumped v5→v6 on a fresh node pull
(2026-07-26) and broke DNS for cross-node sources (v6 defaults to
`listeningMode=LOCAL`) — fixed via `FTLCONF_dns_listeningMode=ALL`.
pinned. vaultwarden is pinned (1.35.4).
- searxng keeps its helm-generated name suffix (`searxng-1723974683`) because
deployment selectors are immutable; its helm release record still exists in
the cluster — don't run `helm upgrade/uninstall` on it, this repo is the
@ -106,8 +98,4 @@ to secrets and no delete verbs.
`rpi-master_undervoltage_CNI_corruption_incident-2026-07-25.md`.
- 2026-07-26: initial import of manifests from live cluster; HA ingress
defaultBackend fixed; unifiedstreaming-svc LoadBalancer→ClusterIP;
pihole/USP secrets migrated to secretKeyRef in-cluster.
- 2026-07-26: pihole surprise v5→v6 upgrade via `:latest` on a fresh node
pull; image pinned by digest, `FTLCONF_dns_listeningMode=ALL` +
`FTLCONF_webserver_api_password` (v6 ignores WEBPASSWORD), readiness probe
on :53 added. Gitea Actions pipeline + deployer RBAC added.
pihole/USP secrets moved to secretKeyRef (pending first apply).

View File

@ -1,13 +1,7 @@
# Pi-hole v6 (image pinned by digest = Core v6.2.2 — :latest bit us on
# 2026-07-26 when a fresh node pull silently jumped v5→v6).
# v6 notes:
# - FTLCONF_dns_listeningMode=ALL is REQUIRED in k8s: the v6 default (LOCAL)
# only answers queries from directly-attached subnets, which silently drops
# all cross-node svclb traffic.
# - v6 ignores the old WEBPASSWORD env; the admin password is
# FTLCONF_webserver_api_password, fed from the `pihole-admin` secret —
# create it (see secret.example.yaml) BEFORE applying.
# - Readiness probe on :53 so rollouts wait for FTL to actually serve DNS.
# DIVERGENCE from live state (intentional): the live deployment has WEBPASSWORD
# as a plaintext env value. Here it comes from the `pihole-admin` secret —
# create it (see secret.example.yaml) BEFORE applying this, or the pod will
# fail with CreateContainerConfigError.
apiVersion: apps/v1
kind: Deployment
metadata:
@ -46,14 +40,12 @@ spec:
- raspi
containers:
- name: pihole
image: pihole/pihole@sha256:e28e239f55e648a9d32c8f065650acfe987ddebf1cd5f64f1c071e8716156ceb # Core v6.2.2
image: pihole/pihole:latest
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: America/Los_Angeles
- name: FTLCONF_dns_listeningMode
value: ALL
- name: FTLCONF_webserver_api_password
- name: WEBPASSWORD
valueFrom:
secretKeyRef:
name: pihole-admin
@ -80,12 +72,6 @@ spec:
memory: 2Gi
securityContext:
privileged: true
readinessProbe:
tcpSocket:
port: 53
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
volumeMounts:
- name: gv-pihole
mountPath: /etc/pihole

View File

@ -1,51 +0,0 @@
# 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