Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .agents/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,15 @@ operational invariants that template changes must preserve.
inlines non-secret config, narrows secrets to database password and optional
encryption key, skips provisioning, and mounts a hook-scoped copy of
`authup.yml`. The configuration ConfigMap and migration NetworkPolicy have
weight -5; the Job has weight 0.
22. **`useHelmHooks=false` is ArgoCD-only.** It emits PreSync resources. Flux
and plain Helm need native hooks or they apply an immutable Job as a normal
resource without correct ordering.
Helm hook-weight -5; the Job has weight 0. Under `useHelmHooks=false` this
extends to every object the Job's pod spec can reference: the built-in
database, the ServiceAccount, and the auth/external-db Secrets all render at
sync-wave -10 so the Job (wave -1) never waits on a resource ArgoCD hasn't
created yet, which for a Job (bounded `backoffLimit`, no self-healing retry
like a Deployment) is a deadlock, not a slow start (issue #30).
22. **`useHelmHooks=false` is ArgoCD-only.** It emits Sync-phase resources
ordered by sync-wave, not PreSync. Flux and plain Helm need native hooks or
they apply an immutable Job as a normal resource without correct ordering.
23. **Checksum annotations follow every consumed input.** Deployments roll on
chart-managed env, Secret, provisioning, configuration and theme changes.
`disableRestartOnChanges` is the explicit escape hatch.
Expand Down
14 changes: 11 additions & 3 deletions .agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ and cache, plus restrictive NetworkPolicies.
- non-empty `server.features.accountConsole`, which moved to
`accountConsole.enabled`
- invalid theme manifests or dangerous trusted-origin globstars
- `commonAnnotations` or `serviceAccount.annotations` setting
`argocd.argoproj.io/sync-wave` under `useHelmHooks=false`, which would
collide with the chart's own wave ordering
on only some of the affected resources

The beta.64 contract script exercises the moved value, split dependencies,
route flags and reserved role env variables directly.
Expand All @@ -62,12 +66,16 @@ The pre-upgrade migration Job must stay narrower than the server Deployment:
- `authup.yml` comes from the hook-scoped configuration ConfigMap
- logs mount at `/var/log/authup`
- the migration NetworkPolicy selects component `migration`, uses the same hook
family, and runs at weight or wave -5 before the Job at 0
family, and runs at Helm hook-weight -5 before the Job at 0, or ArgoCD
sync-wave -5 before the Job at -1
- under `useHelmHooks=false`, the built-in database, the ServiceAccount and the
auth/external-db Secrets all render at sync-wave -10, strictly before the
Job's wave -1 (issue #30: a PreSync Job used to run before all of these)
- fresh-install server env has no `MIGRATION_ENABLED`; upgrade server env has
`MIGRATION_ENABLED=false` when the Job is enabled and the database persists,
but leaves startup migration enabled for non-persistent built-in databases;
with `useHelmHooks=false` every render counts as an upgrade because PreSync
precedes each sync
with `useHelmHooks=false` every render counts as an upgrade because the Job
is a hook on every sync, not just the first

Run both Helm and ArgoCD annotation paths:

Expand Down
19 changes: 14 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,20 @@ Helm creates hooks before regular release resources. The Job therefore:
- mounts a hook-scoped copy of `authup.yml`
- mounts `/var/log/authup`

The hook configuration ConfigMap and migration NetworkPolicy run at weight -5;
the Job runs at 0. This ensures configuration and egress policy exist before the
pod. `useHelmHooks=false` emits ArgoCD PreSync annotations. It is not a Flux or
plain-Helm mode because a normal Job has immutable pod templates and no correct
upgrade ordering.
The hook configuration ConfigMap and migration NetworkPolicy run at Helm
hook-weight -5; the Job runs at 0. This ensures configuration and egress policy
exist before the pod. `useHelmHooks=false` emits ArgoCD annotations instead. It
is not a Flux or plain-Helm mode because a normal Job has immutable pod
templates and no correct upgrade ordering.

Under ArgoCD the Job is a Sync-phase hook (not PreSync), at sync-wave -1: a
PreSync hook runs before every Sync-phase resource, including the built-in
database, which deadlocked a fresh install (issue #30). Everything the Job's
pod spec can reference (the built-in database, the ServiceAccount, and the
auth/external-db Secrets when they carry values the Job needs) renders at wave
-10; the hook-scoped ConfigMap and NetworkPolicy at -5. ArgoCD waits for each
wave to be healthy before starting the next, so the Job always runs after its
own inputs exist, and still before the server Deployment (implicit wave 0).

## 8. Network policy

Expand Down
2 changes: 2 additions & 0 deletions charts/authup/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ annotations:
artifacthub.io/changes: |
- kind: changed
description: Track Authup v1.0.0-beta.65; no image entrypoint, CLI or environment-variable contract changes
- kind: fixed
description: Fix ArgoCD first-sync deadlock when server.migration.enabled and a built-in database are both enabled with useHelmHooks=false
4 changes: 2 additions & 2 deletions charts/authup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ Kubernetes: `>=1.25.0-0`
| server.mfa.enabled | bool | `false` | Enable multi-factor authentication (MFA_ENABLED) |
| server.mfa.required | bool | `false` | Require MFA for every user (MFA_REQUIRED; needs mfa.enabled) |
| server.migration.backoffLimit | int | `3` | Job backoff limit |
| server.migration.enabled | bool | `false` | Run `migration run` as a pre-upgrade hook Job. Recommended for multi-replica deployments (serializes DDL before pods roll). Fresh installs and non-persistent built-in databases migrate at boot regardless. Under useHelmHooks=false the Job is a PreSync hook on the first sync too, so with a built-in database enable it only after that sync |
| server.migration.enabled | bool | `false` | Run `migration run` as a pre-upgrade hook Job. Recommended for multi-replica deployments (serializes DDL before pods roll). Fresh installs and non-persistent built-in databases migrate at boot regardless. Under useHelmHooks=false the Job runs as an ArgoCD Sync-phase hook ordered after the built-in database by sync-wave, so it is safe from the first sync |
| server.migration.podAnnotations | object | `{}` | Job pod annotations |
| server.migration.resources | object | `{}` | Job resources ({} = server resources defaults) |
| server.migration.ttlSecondsAfterFinished | int | `300` | Delete the Job this many seconds after it finishes ("" = keep) |
Expand Down Expand Up @@ -694,7 +694,7 @@ Kubernetes: `>=1.25.0-0`
| smtp.connectionString | string | `""` | SMTP connection string (smtp(s)://user:pass@host:port); stored in a chart-managed secret |
| smtp.existingSecret | string | `""` | Existing secret holding the SMTP connection string (tpl-rendered) |
| smtp.existingSecretKey | string | `"smtp-connection-string"` | Key inside smtp.existingSecret holding the connection string |
| useHelmHooks | bool | `true` | Render Helm hook annotations on the migration Job. Set false only for ArgoCD, which reads its own PreSync annotations instead (it also understands Helm hooks, so true works there too). Flux and plain helm need true: a plain Job's pod template is immutable, so the next upgrade cannot patch it. |
| useHelmHooks | bool | `true` | Render Helm hook annotations on the migration Job. Set false only for ArgoCD, which reads its own Sync-phase, sync-wave annotations instead. ArgoCD also understands Helm hooks, but maps pre-upgrade to an unordered PreSync hook: with server.migration.enabled and a built-in database, set false so the Job waits for the database instead of deadlocking the first sync. Flux and plain helm need true: a plain Job's pod template is immutable, so the next upgrade cannot patch it. |
| valkey.affinity | object | `{}` | Valkey affinity |
| valkey.auth.password | string | `""` | Valkey password ("" = generate once, keep across upgrades) |
| valkey.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"runAsGroup":999,"runAsNonRoot":true,"runAsUser":999,"seccompProfile":{"type":"RuntimeDefault"}}` | Valkey container security context |
Expand Down
6 changes: 3 additions & 3 deletions charts/authup/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ migrations are serialized before pods roll.

{{- if and .Values.server.enabled .Values.server.migration.enabled (not .Values.useHelmHooks) }}

WARNING: useHelmHooks=false is for ArgoCD PreSync only. Flux and plain Helm
apply the Job as a normal immutable resource, so later upgrades can fail. Use
useHelmHooks=true outside ArgoCD.
WARNING: useHelmHooks=false renders ArgoCD-specific Sync-phase hook
annotations only. Flux and plain Helm apply the Job as a normal immutable
resource, so later upgrades can fail. Use useHelmHooks=true outside ArgoCD.
{{- end }}

{{- if .Values.server.provisioning.enabled }}
Expand Down
3 changes: 3 additions & 0 deletions charts/authup/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ Common annotations applied to every object.
{{- if .context.Values.commonAnnotations -}}
{{ include "authup.tplvalues.render" (dict "value" .context.Values.commonAnnotations "context" .context) }}
{{- end -}}
{{- if and .syncWave (not .context.Values.useHelmHooks) }}
argocd.argoproj.io/sync-wave: {{ .syncWave | quote }}
{{- end -}}
{{- end -}}

{{/*
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/mysql/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
labels: {{- include "authup.labels" (dict "context" $ "component" "mysql") | nindent 4 }}
annotations:
helm.sh/resource-policy: keep
{{- include "authup.annotations" (dict "context" $) | nindent 4 }}
{{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
type: Opaque
data:
password: {{ include "authup.secret.rawValue" (dict "secret" $secretName "key" "password" "value" .Values.mysql.auth.password "length" 32 "context" $) | b64enc | quote }}
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/mysql/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
name: {{ include "authup.mysql.fullname" . }}
namespace: {{ include "authup.namespace" . | quote }}
labels: {{- include "authup.labels" (dict "context" $ "component" "mysql") | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
spec:
type: ClusterIP
ports:
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/mysql/statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ metadata:
name: {{ include "authup.mysql.fullname" . }}
namespace: {{ include "authup.namespace" . | quote }}
labels: {{- include "authup.labels" (dict "context" $ "component" "mysql") | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
spec:
serviceName: {{ include "authup.mysql.fullname" . }}
replicas: 1
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/postgresql/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:
labels: {{- include "authup.labels" (dict "context" $ "component" "postgresql") | nindent 4 }}
annotations:
helm.sh/resource-policy: keep
{{- include "authup.annotations" (dict "context" $) | nindent 4 }}
{{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
type: Opaque
data:
password: {{ include "authup.secret.rawValue" (dict "secret" $secretName "key" "password" "value" .Values.postgresql.auth.password "length" 32 "context" $) | b64enc | quote }}
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/postgresql/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
name: {{ include "authup.postgresql.fullname" . }}
namespace: {{ include "authup.namespace" . | quote }}
labels: {{- include "authup.labels" (dict "context" $ "component" "postgresql") | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
spec:
type: ClusterIP
ports:
Expand Down
2 changes: 1 addition & 1 deletion charts/authup/templates/postgresql/statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ metadata:
name: {{ include "authup.postgresql.fullname" . }}
namespace: {{ include "authup.namespace" . | quote }}
labels: {{- include "authup.labels" (dict "context" $ "component" "postgresql") | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }}
annotations: {{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
spec:
serviceName: {{ include "authup.postgresql.fullname" . }}
replicas: 1
Expand Down
6 changes: 5 additions & 1 deletion charts/authup/templates/secret-db.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ inline password still lands in a chart-managed Secret — never in a pod env
literal. Deliberately no generation fallback: validations.yaml fails when
neither password nor existingSecret is given (the chart will not invent a
password for a database it does not manage).

The migration Job mounts this Secret's password too. Under useHelmHooks=false
it renders at sync-wave -10, same as the built-in database, so it exists
before the Job's wave -1.
*/}}
{{- if and (not .Values.postgresql.enabled) (not .Values.mysql.enabled) .Values.externalDatabase.host .Values.externalDatabase.password (not .Values.externalDatabase.existingSecret) }}
{{- $secretName := printf "%s-externaldb" (include "authup.fullname" .) }}
Expand All @@ -15,7 +19,7 @@ metadata:
labels: {{- include "authup.labels" (dict "context" $) | nindent 4 }}
annotations:
helm.sh/resource-policy: keep
{{- include "authup.annotations" (dict "context" $) | nindent 4 }}
{{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
type: Opaque
data:
password: {{ .Values.externalDatabase.password | b64enc | quote }}
Expand Down
8 changes: 7 additions & 1 deletion charts/authup/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{{- if include "authup.auth.createSecret" . }}
{{/*
When auth.secretsEncryptionKey is set inline, the migration Job mounts
SECRETS_ENCRYPTION_KEY from this Secret. Under useHelmHooks=false it renders
at sync-wave -10, same as the built-in database, so it exists before the Job's
wave -1.
*/}}
{{- $secretName := include "authup.fullname" . }}
apiVersion: v1
kind: Secret
Expand All @@ -8,7 +14,7 @@ metadata:
labels: {{- include "authup.labels" (dict "context" $) | nindent 4 }}
annotations:
helm.sh/resource-policy: keep
{{- include "authup.annotations" (dict "context" $) | nindent 4 }}
{{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
type: Opaque
data:
{{ .Values.auth.secretKeys.adminPasswordKey }}: {{ include "authup.secret.rawValue" (dict "secret" $secretName "key" .Values.auth.secretKeys.adminPasswordKey "value" .Values.auth.adminPassword "length" 32 "context" $) | b64enc | quote }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Rendered from authup.server.configurationContent, the single source the release
ConfigMap uses too, so migration and server pods can never read different files.
Hook weight -5 puts it ahead of the Job's 0 (helm applies the hook-succeeded
delete policy only after every hook in the event has run, so it outlives the
Job); the ArgoCD branch mirrors that with sync-wave -5.
Job); the ArgoCD branch mirrors that with sync-wave -5, as a Sync-phase hook
rather than a PreSync one, so it also runs after the built-in database's wave
-10 and still ahead of the Job at -1.
*/}}
apiVersion: v1
kind: ConfigMap
Expand All @@ -29,7 +31,7 @@ metadata:
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
helm.sh/hook-weight: "-5"
{{- else }}
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-5"
{{- end }}
Expand Down
5 changes: 3 additions & 2 deletions charts/authup/templates/server/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ spec:
value: "false"
{{- end }}
{{- /* The Job precedes the pods on every helm upgrade and on every ArgoCD
sync (PreSync). Only a database that survives the rollout keeps the
Job's work, so ephemeral built-in stores keep boot migration on. */}}
sync (a lower sync-wave). Only a database that survives the rollout
keeps the Job's work, so ephemeral built-in stores keep boot
migration on. */}}
{{- $dbPersists := or (not (or .Values.postgresql.enabled .Values.mysql.enabled)) (and .Values.postgresql.enabled (or .Values.postgresql.persistence.enabled .Values.postgresql.persistence.existingClaim)) (and .Values.mysql.enabled (or .Values.mysql.persistence.enabled .Values.mysql.persistence.existingClaim)) }}
{{- if and .Values.server.migration.enabled (or .Release.IsUpgrade (not .Values.useHelmHooks)) $dbPersists }}
- name: MIGRATION_ENABLED
Expand Down
15 changes: 14 additions & 1 deletion charts/authup/templates/server/migration-job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ migrates at boot anyway. On upgrades the Job serializes DDL before new pods
roll, which matters for multi-replica deployments (MySQL DDL is not
transactional). The env ConfigMap content is INLINED so the hook never runs
against the previous release's ConfigMap.

ArgoCD has no pre-upgrade concept, so useHelmHooks=false hits the same
fresh-install problem a different way: a PreSync hook runs before every
Sync-phase resource, including the built-in database, so the Job used to fail
against a database that could never appear. The fix keeps the Job a hook (for
BeforeHookCreation delete-and-recreate on every sync) but moves it into the
Sync phase at sync-wave -1, ordered by wave instead of by phase. Everything
the Job's pod spec can reference (the built-in database, the ServiceAccount,
and the auth/external-db Secrets when they carry the values it needs) renders
at wave -10; the hook-scoped ConfigMap and NetworkPolicy at -5; so the Job
always runs after all of its own inputs exist and are healthy, and still
before the server Deployment (implicit wave 0).
*/}}
apiVersion: batch/v1
kind: Job
Expand All @@ -28,8 +40,9 @@ metadata:
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
helm.sh/hook-weight: "0"
{{- else }}
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
{{- end }}
spec:
backoffLimit: {{ .Values.server.migration.backoffLimit }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ metadata:
helm.sh/hook-delete-policy: before-hook-creation
helm.sh/hook-weight: "-5"
{{- else }}
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-5"
{{- end }}
Expand Down
10 changes: 9 additions & 1 deletion charts/authup/templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
{{- if .Values.serviceAccount.create }}
{{/*
The migration Job (server/migration-job.yaml) always runs as this
ServiceAccount. Under useHelmHooks=false it renders at sync-wave -10, same as
the built-in database, so it exists before the Job's wave -1: a ServiceAccount
missing when a pod is admitted is a pod-creation failure, which never counts
toward the Job's backoffLimit and would hang the sync forever instead of
failing loudly.
*/}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "authup.serviceAccountName" . }}
namespace: {{ include "authup.namespace" . | quote }}
labels: {{- include "authup.labels" (dict "context" $) | nindent 4 }}
annotations:
{{- include "authup.annotations" (dict "context" $) | nindent 4 }}
{{- include "authup.annotations" (dict "context" $ "syncWave" "-10") | nindent 4 }}
Comment thread
tada5hi marked this conversation as resolved.
{{- if .Values.serviceAccount.annotations }}
{{- include "authup.tplvalues.render" (dict "value" .Values.serviceAccount.annotations "context" $) | nindent 4 }}
{{- end }}
Expand Down
Loading
Loading