Import manifests reverse-engineered from live cluster

Captured 2026-07-26 from rpi-master (k3s v1.30.3) and cleaned of runtime
fields. Six apps as per-app kustomizations: vaultwarden, searxng,
home-assistant, nginx, pihole(+unbound), unified-streaming.

Intentional divergences from live state:
- pihole WEBPASSWORD and USP_LICENSE_KEY moved from inline plaintext env
  to secretKeyRef (secrets gitignored; templates in secret.example.yaml)
- HA ingress defaultBackend fixed (pointed at nonexistent service)
- unifiedstreaming-svc kept as ClusterIP (LoadBalancer could never bind
  port 80 behind svclb-traefik)

Validated against the live cluster with kubectl apply --dry-run=server:
no immutable-field conflicts; one-time kubectl replace procedure for the
two env->secretKeyRef migrations documented in README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jasonwitty
2026-07-26 02:13:56 -07:00
parent ec9b422f87
commit 8afac39d03
40 changed files with 1179 additions and 1 deletions
+86
View File
@@ -0,0 +1,86 @@
# 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:
name: pihole
namespace: pihole
labels:
app: pihole
spec:
replicas: 1
selector:
matchLabels:
app: pihole
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
template:
metadata:
labels:
app: pihole
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: cpu
operator: In
values:
- arm
- key: model
operator: In
values:
- raspi
containers:
- name: pihole
image: pihole/pihole:latest
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: America/Los_Angeles
- name: WEBPASSWORD
valueFrom:
secretKeyRef:
name: pihole-admin
key: WEBPASSWORD
- name: PIHOLE_UID
value: "0"
ports:
- containerPort: 53
protocol: TCP
- containerPort: 53
protocol: UDP
- containerPort: 67
protocol: UDP
- containerPort: 80
protocol: TCP
- containerPort: 443
protocol: TCP
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 2Gi
securityContext:
privileged: true
volumeMounts:
- name: gv-pihole
mountPath: /etc/pihole
- name: gv-pihole-dnsmasq-volume
mountPath: /etc/dnsmasq.d
volumes:
- name: gv-pihole
persistentVolumeClaim:
claimName: pihole-gvolume0
- name: gv-pihole-dnsmasq-volume
persistentVolumeClaim:
claimName: pihole-dnsmasq-volume
+54
View File
@@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: unbound
namespace: pihole
labels:
app: unbound
spec:
replicas: 3
selector:
matchLabels:
app: unbound
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
template:
metadata:
labels:
app: unbound
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: cpu
operator: In
values:
- arm
- key: model
operator: In
values:
- raspi
containers:
- name: unbound
image: docker.io/mvance/unbound-rpi:latest
imagePullPolicy: Always
ports:
- name: svc-53-udp
containerPort: 53
protocol: UDP
- name: svc-53-tcp
containerPort: 53
protocol: TCP
resources:
requests:
cpu: 250m
memory: 50Mi
limits:
cpu: "1"
memory: 1Gi
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# secret.example.yaml is intentionally NOT listed — copy it to secret.yaml
# (gitignored) and apply manually once per cluster.
resources:
- namespace.yaml
- storage.yaml
- deployment-pihole.yaml
- deployment-unbound.yaml
- service.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: pihole
+13
View File
@@ -0,0 +1,13 @@
# Copy to secret.yaml (gitignored), set the real admin password, apply once:
# kubectl apply -f secret.yaml
# Or create directly without a file:
# kubectl create secret generic pihole-admin -n pihole \
# --from-literal=WEBPASSWORD='...'
apiVersion: v1
kind: Secret
metadata:
name: pihole-admin
namespace: pihole
type: Opaque
stringData:
WEBPASSWORD: CHANGE-ME
+53
View File
@@ -0,0 +1,53 @@
# pihole-svc is type LoadBalancer → k3s klipper-lb (svclb) binds host ports
# 8000/53/67 on the nodes it schedules on. NodePorts are pinned so they
# survive re-creation.
apiVersion: v1
kind: Service
metadata:
name: pihole-svc
namespace: pihole
spec:
type: LoadBalancer
externalTrafficPolicy: Cluster
selector:
app: pihole
ports:
- name: http-admin
port: 8000
targetPort: 80
nodePort: 31120
protocol: TCP
- name: tcp-53
port: 53
targetPort: 53
nodePort: 32580
protocol: TCP
- name: udp-53
port: 53
targetPort: 53
nodePort: 31281
protocol: UDP
- name: udp-67
port: 67
targetPort: 67
nodePort: 31025
protocol: UDP
---
apiVersion: v1
kind: Service
metadata:
name: unbound-service
namespace: pihole
spec:
type: ClusterIP
selector:
app: unbound
ports:
- name: udp-53
port: 53
targetPort: 53
protocol: UDP
- name: tcp-53
port: 53
targetPort: 53
protocol: TCP
+55
View File
@@ -0,0 +1,55 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: pihole-gvolume0-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
hostPath:
path: /mnt/gvolume0/pihole
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pihole-dnsmasq-volume-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
hostPath:
path: /mnt/gvolume0/pihole-dnsmasq-volume
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pihole-gvolume0
namespace: pihole
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: ""
volumeName: pihole-gvolume0-pv
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pihole-dnsmasq-volume
namespace: pihole
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: ""
volumeName: pihole-dnsmasq-volume-pv