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. # Real secrets never go in git — only *.example.yaml templates do.
apps/**/secret.yaml apps/**/secret.yaml
*.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` | | `usp-license` | unified-streaming | `USP_LICENSE_KEY` |
| `searxng-1723974683-config` | default | searxng `settings.yml` | | `searxng-1723974683-config` | default | searxng `settings.yml` |
~~**One-time migration (pihole + unified-streaming)**~~ — DONE 2026-07-26: **One-time migration (pihole + unified-streaming):** the live cluster (as of
secrets `pihole-admin` and `usp-license` created in-cluster and both 2026-07-26) has WEBPASSWORD / USP_LICENSE_KEY as inline plaintext `value`s.
deployments switched to `secretKeyRef` via `kubectl replace` (a plain `kubectl apply` (client- or server-side) cannot patch an env var from `value`
`kubectl apply` cannot patch an env var from `value` to `valueFrom`). to `valueFrom` — the API rejects the merged object. Validated procedure:
`kubectl apply -k .` now converges cleanly (verified `--dry-run=server`).
## 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: After that, `kubectl apply -k .` works cleanly for everything (verified with
`--dry-run=server` against the live cluster, 2026-07-26 — zero immutable-field
- **PRs**`kubectl apply -k . --dry-run=server` (validation only) conflicts).
- **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) ## 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) ## Known quirks (faithful to live state, fix at leisure)
- Most images are `:latest`/`:stable` — deploys are not reproducible until - Most images are `:latest`/`:stable` — deploys are not reproducible until
pinned. vaultwarden is pinned (1.35.4); pihole is pinned by digest (Core pinned. vaultwarden is pinned (1.35.4).
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`.
- searxng keeps its helm-generated name suffix (`searxng-1723974683`) because - searxng keeps its helm-generated name suffix (`searxng-1723974683`) because
deployment selectors are immutable; its helm release record still exists in 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 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`. `rpi-master_undervoltage_CNI_corruption_incident-2026-07-25.md`.
- 2026-07-26: initial import of manifests from live cluster; HA ingress - 2026-07-26: initial import of manifests from live cluster; HA ingress
defaultBackend fixed; unifiedstreaming-svc LoadBalancer→ClusterIP; defaultBackend fixed; unifiedstreaming-svc LoadBalancer→ClusterIP;
pihole/USP secrets migrated to secretKeyRef in-cluster. pihole/USP secrets moved to secretKeyRef (pending first apply).
- 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.

View File

@ -1,13 +1,7 @@
# Pi-hole v6 (image pinned by digest = Core v6.2.2 — :latest bit us on # DIVERGENCE from live state (intentional): the live deployment has WEBPASSWORD
# 2026-07-26 when a fresh node pull silently jumped v5→v6). # as a plaintext env value. Here it comes from the `pihole-admin` secret —
# v6 notes: # create it (see secret.example.yaml) BEFORE applying this, or the pod will
# - FTLCONF_dns_listeningMode=ALL is REQUIRED in k8s: the v6 default (LOCAL) # fail with CreateContainerConfigError.
# 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.
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -46,14 +40,12 @@ spec:
- raspi - raspi
containers: containers:
- name: pihole - name: pihole
image: pihole/pihole@sha256:e28e239f55e648a9d32c8f065650acfe987ddebf1cd5f64f1c071e8716156ceb # Core v6.2.2 image: pihole/pihole:latest
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
env: env:
- name: TZ - name: TZ
value: America/Los_Angeles value: America/Los_Angeles
- name: FTLCONF_dns_listeningMode - name: WEBPASSWORD
value: ALL
- name: FTLCONF_webserver_api_password
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: pihole-admin name: pihole-admin
@ -80,12 +72,6 @@ spec:
memory: 2Gi memory: 2Gi
securityContext: securityContext:
privileged: true privileged: true
readinessProbe:
tcpSocket:
port: 53
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
volumeMounts: volumeMounts:
- name: gv-pihole - name: gv-pihole
mountPath: /etc/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