Compare commits
No commits in common. "fe72f1e85a3fc7096d90a785c60d45a893b7cbc3" and "8afac39d0350c08b36019a39f7e278a94c507476" have entirely different histories.
fe72f1e85a
...
8afac39d03
@ -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
1
.gitignore
vendored
@ -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
|
|
||||||
|
|||||||
42
README.md
42
README.md
@ -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.
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
Loading…
Reference in New Issue
Block a user