fix(renderer): Recreate strategy for Deployments mounting RWO PVCs

Any single-replica app with a `storage:` entry mounts a ReadWriteOnce Longhorn
PVC. The default RollingUpdate strategy starts the new pod before terminating the
old one, but the old pod still holds the RWO volume -> the new pod hangs in
ContainerCreating and the rollout deadlocks (observed on gitea's update). Emit
spec.strategy.type=Recreate whenever storage is present so the old pod releases
the PVC first. Non-storage apps (and VMs) keep the default (no strategy emitted).

Re-rendered apps: gitea, pihole, sonarr, radarr, plex, qbittorrent, jackett now
carry strategy: Recreate. Updated the egress-class L1 render goldens (storage
fixture) accordingly; all L1 render checks pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 4.8 committed 2026-08-25 18:14:45 -05:00
1 parent 73c4475d34
commit d32b5c4ec0
10 files changed
+36 -11

No files matched your search

+2
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: gitea app.kubernetes.io/name: gitea
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: gitea app.kubernetes.io/name: gitea
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: jackett app.kubernetes.io/name: jackett
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: jackett app.kubernetes.io/name: jackett
@@ -123,7 +125,7 @@ spec:
- port: '9117' - port: '9117'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (vpn class, R-NET-05): jackett-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): jackett-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: pihole app.kubernetes.io/name: pihole
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: pihole app.kubernetes.io/name: pihole
@@ -123,7 +125,7 @@ spec:
- port: '80' - port: '80'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (internet class, R-NET-05): pihole-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): pihole-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: plex app.kubernetes.io/name: plex
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: plex app.kubernetes.io/name: plex
@@ -120,7 +122,7 @@ spec:
- port: '32400' - port: '32400'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (internet class, R-NET-05): plex-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): plex-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: qbittorrent app.kubernetes.io/name: qbittorrent
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: qbittorrent app.kubernetes.io/name: qbittorrent
@@ -123,7 +125,7 @@ spec:
- port: '8080' - port: '8080'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (vpn class, R-NET-05): qbittorrent-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): qbittorrent-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: radarr app.kubernetes.io/name: radarr
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: radarr app.kubernetes.io/name: radarr
@@ -165,7 +167,7 @@ spec:
- port: '7878' - port: '7878'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (vpn class, R-NET-05): radarr-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): radarr-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+3 -1
View File
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr
@@ -165,7 +167,7 @@ spec:
- port: '8989' - port: '8989'
protocol: TCP protocol: TCP
--- ---
# CiliumNetworkPolicy internet/world egress (vpn class, R-NET-05): sonarr-allow-internet-egress # CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): sonarr-allow-internet-egress
apiVersion: cilium.io/v2 apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy kind: CiliumNetworkPolicy
metadata: metadata:
+12 -5
View File
@@ -243,6 +243,17 @@ def r_deployment(s: dict) -> dict:
if ann: if ann:
tmpl_meta["annotations"] = ann tmpl_meta["annotations"] = ann
spec = {"replicas": 1}
# An app with persistent storage mounts a ReadWriteOnce Longhorn PVC. The default
# RollingUpdate strategy tries to start the NEW pod before terminating the old one, but
# the old pod still holds the RWO volume -> the new pod is stuck ContainerCreating and
# the rollout deadlocks (single-replica app can never hand the volume over). Recreate
# terminates the old pod first, freeing the PVC for the new one (R-STATE-01/02).
if s["storage"]:
spec["strategy"] = {"type": "Recreate"}
spec["selector"] = {"matchLabels": {"app.kubernetes.io/name": s["name"]}}
spec["template"] = {"metadata": tmpl_meta, "spec": pod_spec}
return { return {
"apiVersion": "apps/v1", "apiVersion": "apps/v1",
"kind": "Deployment", "kind": "Deployment",
@@ -251,11 +262,7 @@ def r_deployment(s: dict) -> dict:
"namespace": s["name"], "namespace": s["name"],
"labels": {"app.kubernetes.io/name": s["name"]}, "labels": {"app.kubernetes.io/name": s["name"]},
}, },
"spec": { "spec": spec,
"replicas": 1,
"selector": {"matchLabels": {"app.kubernetes.io/name": s["name"]}},
"template": {"metadata": tmpl_meta, "spec": pod_spec},
},
} }
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr
@@ -61,6 +61,8 @@ metadata:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: sonarr app.kubernetes.io/name: sonarr