ghp: fix first-install ordering deadlock via sync-waves
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful

The migrate Job was a PreSync hook but connects to postgres-rw, whose CNPG
Cluster + generated postgres-app secret apply in the Sync phase (after all
PreSync hooks). On a fresh install migrate ran before Postgres existed, failed,
exhausted backoffLimit, failed PreSync, and blocked the Sync phase that creates
the DB.

- Move migrate off PreSync to a Sync-phase hook at sync-wave 1.
- Put the CNPG Cluster + backup resources, VSO auth/secrets, and Certificate at
  wave 0 so the DB is Healthy (and creds/cert exist) before migrate runs.
- Put the Deployment + Service/Gateway/HTTPRoute/PDB/VMServiceScrape at wave 2
  so serve starts after migrate completes.
- Add a writable /tmp emptyDir to the serve container and migrate Job (root FS
  is read-only) so codeload/staging writes cannot crash the process.
This commit is contained in:
2026-08-13 19:53:09 +10:00
parent 1d5e8edbdd
commit 42c1794d0e
12 changed files with 53 additions and 2 deletions
+2
View File
@@ -9,6 +9,8 @@ metadata:
labels:
app.kubernetes.io/name: ghp
app.kubernetes.io/instance: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
secretName: ghp-tls
issuerRef:
+6
View File
@@ -6,6 +6,8 @@ kind: ObjectStoreUser
metadata:
name: cnpg-ghp-backup
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
displayName: "CNPG backup owner (ghp)"
uid: cnpg-ghp-backup
@@ -18,6 +20,8 @@ kind: Bucket
metadata:
name: cnpg-ghp
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
placementTarget: ec
bucketName: cnpg-ghp
@@ -35,6 +39,8 @@ kind: ScheduledBackup
metadata:
name: cnpg-ghp-nightly
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
schedule: "0 50 1 * * *"
immediate: false
+4
View File
@@ -4,6 +4,10 @@ kind: Cluster
metadata:
name: postgres
namespace: ghp
annotations:
# Wave 0: DB (and the generated postgres-app Secret) must be Healthy before
# the wave-1 migrate Job runs. ArgoCD gates on the Cluster's health status.
argocd.argoproj.io/sync-wave: "0"
spec:
affinity:
podAntiAffinityType: preferred
+10 -1
View File
@@ -5,6 +5,8 @@ metadata:
name: ghp
namespace: ghp
annotations:
# Wave 2: serve only after the wave-1 migrate Job completes.
argocd.argoproj.io/sync-wave: "2"
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "ghp-github-app,ghp-app,ghp-tls,postgres-app"
spec:
@@ -35,7 +37,7 @@ spec:
image: artifactapi.k8s.syd1.au.unkin.net/ghcr/goodtune/ghp:0.20.0
imagePullPolicy: IfNotPresent
# Drop the image's default --migrate so replicas never race migrations;
# schema is applied by the PreSync hook Job instead.
# schema is applied by the wave-1 migrate hook Job instead.
command: ["/ghp", "serve"]
ports:
- containerPort: 8443
@@ -93,6 +95,8 @@ spec:
- name: tls
mountPath: /etc/ghp/tls
readOnly: true
- name: tmp
mountPath: /tmp
livenessProbe:
failureThreshold: 3
httpGet:
@@ -133,4 +137,9 @@ spec:
- name: tls
secret:
secretName: ghp-tls
# Writable scratch: root FS is read-only. Disk-backed (not memory medium)
# so codeload tarball staging doesn't count against the pod memory limit.
- name: tmp
emptyDir:
sizeLimit: 2Gi
restartPolicy: Always
+1
View File
@@ -9,6 +9,7 @@ metadata:
labels:
traefik.io/instance: internal
annotations:
argocd.argoproj.io/sync-wave: "2"
external-dns.alpha.kubernetes.io/hostname: ghp.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: ghp
+2
View File
@@ -4,6 +4,8 @@ kind: HTTPRoute
metadata:
name: ghp-route
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
hostnames:
- ghp.k8s.syd1.au.unkin.net
+16 -1
View File
@@ -2,14 +2,21 @@
# Runs the schema migration once per sync, before the Deployment rolls, so the
# serve replicas never race migrations. Deleted before each re-create so a new
# image/version re-runs it.
#
# Sync-phase hook at wave 1 (NOT PreSync): the CNPG Cluster + generated
# postgres-app Secret apply at wave 0 and ArgoCD waits for the Cluster to be
# Healthy before starting wave 1, so on a fresh install Postgres exists before
# migrate connects. (A PreSync hook would run before the Sync phase that creates
# the DB, deadlocking the first install.)
apiVersion: batch/v1
kind: Job
metadata:
name: ghp-migrate
namespace: ghp
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "1"
spec:
backoffLimit: 6
ttlSecondsAfterFinished: 600
@@ -48,6 +55,9 @@ spec:
key: password
- name: GHP_DATABASE_DSN
value: "postgres://$(GHP_DB_USER):$(GHP_DB_PASSWORD)@postgres-rw.ghp.svc:5432/ghp?sslmode=require"
volumeMounts:
- name: tmp
mountPath: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
@@ -61,3 +71,8 @@ spec:
requests:
cpu: 250m
memory: 256Mi
volumes:
# Writable scratch: root FS is read-only, so give the migrator a /tmp.
- name: tmp
emptyDir:
sizeLimit: 256Mi
+2
View File
@@ -4,6 +4,8 @@ kind: PodDisruptionBudget
metadata:
name: ghp
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
minAvailable: 1
selector:
+2
View File
@@ -6,6 +6,8 @@ metadata:
namespace: ghp
labels:
app: ghp
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
internalTrafficPolicy: Cluster
ports:
+2
View File
@@ -4,6 +4,8 @@ kind: VaultAuth
metadata:
name: default
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
allowedNamespaces:
- ghp
+4
View File
@@ -6,6 +6,8 @@ kind: VaultStaticSecret
metadata:
name: ghp-github-app
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
@@ -24,6 +26,8 @@ kind: VaultStaticSecret
metadata:
name: ghp-app
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
+2
View File
@@ -7,6 +7,8 @@ kind: VMServiceScrape
metadata:
name: ghp
namespace: ghp
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
selector:
matchLabels: