From 42f1ca397e985f1b8c6a956681387cb5446d1c21 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 4 Aug 2026 09:19:13 +0200 Subject: [PATCH 1/3] feat(authup): mount an operator console theme Adds `server.theme`, so rebranding the served consoles is a values change rather than a hand-written ConfigMap plus two volume stanzas: server: theme: enabled: true files: theme.json: | {"version": 1, "tokens": {"--authup-periwinkle": "#c0392b"}} assets/theme.css: | .a-auth-shell-card { border-radius: 2px; } The volume is mounted at /etc/authup/theme and THEME_DIRECTORY_PATH plus THEME_FRAGMENTS_ENABLED are derived from it, mirroring the existing `server.provisioning` UX (files map, existingConfigMap, tpl-rendered content). A ConfigMap key cannot contain "/", but the theme layout has subdirectories, so keys are flattened to "__" and projected back by the volume's items list. The operator writes "assets/theme.css" and never sees the encoding. `existingConfigMapItems` exposes the same projection for an existing ConfigMap, which is also the route for binary assets (binaryData) that a values map cannot express. The theme volume is deliberately NOT part of the shared authup.server.volumes helper. The migration Job is a pre-upgrade hook and hooks precede regular resources, so on the upgrade that first enables theming it would reference a ConfigMap that does not exist yet and hang. A migration run has no use for the theme either way. (The same shape exists for `server.provisioning`, which this change deliberately leaves alone.) Render-time fails, in the chart's fail-loud style: enabled with no content, files and existingConfigMap together, existingConfigMapItems without existingConfigMap, and file keys that are absolute, traverse out of the theme root, or contain the reserved "__". Theming's dominant failure mode is a page that looks exactly like an un-themed page, so none of these may render silently inert. Whole-volume projection rather than subPath: a subPath mount is frozen until the pod restarts, which would destroy authup's live theme reload. Requires an authup image that supports THEME_DIRECTORY_PATH. Older images ignore the variable, so enabling this against one is inert rather than broken. With the theme off the rendered manifests are unchanged. --- charts/authup/README.md | 5 + charts/authup/ci/theme-values.yaml | 28 +++++ charts/authup/templates/_server-env.tpl | 108 +++++++++++++++++- .../templates/server/configmap-theme.yaml | 18 +++ .../authup/templates/server/deployment.yaml | 9 ++ charts/authup/values.schema.json | 50 ++++++++ charts/authup/values.yaml | 23 ++++ 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 charts/authup/ci/theme-values.yaml create mode 100644 charts/authup/templates/server/configmap-theme.yaml diff --git a/charts/authup/README.md b/charts/authup/README.md index 570f2b9..84b75d8 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -405,6 +405,11 @@ Kubernetes: `>=1.25.0-0` | server.startupProbe.successThreshold | int | `1` | | | server.startupProbe.timeoutSeconds | int | `5` | | | server.terminationGracePeriodSeconds | int | `30` | Pod termination grace period (server-core tears down within ~10s after signal) | +| server.theme.enabled | bool | `false` | Mount an operator theme for the served consoles (the auth console and the account console). Requires an authup image that supports THEME_DIRECTORY_PATH; older images ignore it | +| server.theme.existingConfigMap | string | `""` | Existing ConfigMap holding the theme (tpl-rendered name). Use for binary assets, which cannot be expressed in files | +| server.theme.existingConfigMapItems | list | `[]` | Key -> path projection for existingConfigMap, so its keys can land in subdirectories (e.g. [{key: theme-css, path: assets/theme.css}]). Empty mounts every key flat at the theme root | +| server.theme.files | object | `{}` | Map of path -> file content, relative to the theme root (tpl-rendered). Keys may carry a "/" ("assets/theme.css") and are projected into subdirectories. Only assets/ is served over HTTP. Text only — use existingConfigMap with binaryData for images | +| server.theme.fragmentsEnabled | bool | `false` | Read fragments/head.html and splice it into the console . Raw, unsanitized markup on the identity provider origin, so it is opt-in | | server.tolerations | list | `[]` | Tolerations | | server.topologySpreadConstraints | list | `[]` | Topology spread constraints (a missing labelSelector is filled with the pod's selector labels) | | server.trustProxy | string | `"1"` | TRUST_PROXY setting. The chart defaults to one trusted hop (the ingress), not authup's spoofable trust-everything default | diff --git a/charts/authup/ci/theme-values.yaml b/charts/authup/ci/theme-values.yaml new file mode 100644 index 0000000..344abf0 --- /dev/null +++ b/charts/authup/ci/theme-values.yaml @@ -0,0 +1,28 @@ +# Operator theme mounted from an inline files map, covering the path-keyed +# ConfigMap projection (a "/" key must land in a subdirectory) and the env +# wiring. THEME_DIRECTORY_PATH is ignored by an authup image that predates +# console theming, so the rollout assertion holds either way. +server: + theme: + enabled: true + fragmentsEnabled: true + files: + theme.json: | + { + "version": 1, + "title": "Sign in to ACME", + "stylesheet": "assets/theme.css", + "tokens": { + "--authup-periwinkle": "#c0392b" + }, + "tokensDark": { + "--authup-auth-accent": "#e06c5a" + } + } + assets/theme.css: | + .a-auth-shell-card { border-radius: 2px; } + fragments/head.html: | + + +ui: + enabled: false diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index 2d0dadb..5d84557 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -6,6 +6,7 @@ Strict-boolean variables are always quoted: authup crashes the boot on unparsable boolean strings by design. */}} {{- define "authup.server.configEnv" -}} +{{- include "authup.server.validateTheme" . -}} DB_TYPE: {{ include "authup.database.type" . | quote }} DB_HOST: {{ include "authup.database.host" . | quote }} DB_PORT: {{ include "authup.database.port" . | quote }} @@ -27,6 +28,10 @@ PASSWORD_RECOVERY_ENABLED: {{ .Values.server.features.passwordRecovery | toStrin EMAIL_VERIFICATION_ENABLED: {{ .Values.server.features.emailVerification | toString | quote }} MFA_ENABLED: {{ .Values.server.mfa.enabled | toString | quote }} MFA_REQUIRED: {{ .Values.server.mfa.required | toString | quote }} +{{- if include "authup.server.themeMounted" . }} +THEME_DIRECTORY_PATH: {{ include "authup.server.themeMountPath" . | quote }} +THEME_FRAGMENTS_ENABLED: {{ .Values.server.theme.fragmentsEnabled | toString | quote }} +{{- end }} {{- if .Values.auth.adminPasswordReset }} USER_ADMIN_PASSWORD_RESET: "true" {{- end }} @@ -36,7 +41,7 @@ CLIENT_SYSTEM_ENABLED: "true" CLIENT_SYSTEM_SECRET_RESET: "true" {{- end }} {{- end }} -{{- $reserved := list "DB_TYPE" "DB_HOST" "DB_PORT" "DB_USERNAME" "DB_DATABASE" "DB_PASSWORD" "PUBLIC_URL" "TRUSTED_ORIGINS" "TRUST_PROXY" "REGISTRATION_ENABLED" "PASSWORD_RECOVERY_ENABLED" "EMAIL_VERIFICATION_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "USER_ADMIN_PASSWORD" "USER_ADMIN_PASSWORD_RESET" "CLIENT_SYSTEM_ENABLED" "CLIENT_SYSTEM_SECRET" "CLIENT_SYSTEM_SECRET_RESET" "REDIS" "SMTP" "SECRETS_ENCRYPTION_KEY" }} +{{- $reserved := list "DB_TYPE" "DB_HOST" "DB_PORT" "DB_USERNAME" "DB_DATABASE" "DB_PASSWORD" "PUBLIC_URL" "TRUSTED_ORIGINS" "TRUST_PROXY" "REGISTRATION_ENABLED" "PASSWORD_RECOVERY_ENABLED" "EMAIL_VERIFICATION_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" "USER_ADMIN_PASSWORD" "USER_ADMIN_PASSWORD_RESET" "CLIENT_SYSTEM_ENABLED" "CLIENT_SYSTEM_SECRET" "CLIENT_SYSTEM_SECRET_RESET" "REDIS" "SMTP" "SECRETS_ENCRYPTION_KEY" }} {{- range $key, $value := .Values.server.config }} {{- if has $key $reserved }} {{- fail (printf "authup: server.config.%s collides with a first-class chart value — set it through the dedicated value instead." $key) }} @@ -138,6 +143,107 @@ provisioning files, config file). {{- end }} {{- end -}} +{{/* +Theme volume / volumeMount, deliberately NOT part of the shared server +helpers: the migration Job is a pre-upgrade HOOK, and hooks precede regular +resources, so on the upgrade that first enables theming it would reference a +ConfigMap that does not exist yet and hang. A migration run has no use for +the theme either way. +*/}} +{{- define "authup.server.themeVolumeMounts" -}} +{{- if include "authup.server.themeMounted" . }} +- name: theme + mountPath: {{ include "authup.server.themeMountPath" . }} + readOnly: true +{{- end }} +{{- end -}} + +{{- define "authup.server.themeVolumes" -}} +{{- if include "authup.server.themeMounted" . }} +- name: theme + configMap: + name: {{ include "authup.server.themeConfigMapName" . }} + {{- /* Whole-volume projection on purpose: a subPath mount is frozen + until the pod restarts, which would destroy authup's live theme + reload. */}} + {{- if .Values.server.theme.existingConfigMap }} + {{- with .Values.server.theme.existingConfigMapItems }} + items: {{- include "authup.tplvalues.render" (dict "value" . "context" $) | nindent 6 }} + {{- end }} + {{- else }} + items: + {{- range $path, $content := .Values.server.theme.files }} + - key: {{ include "authup.server.themeConfigMapKey" $path }} + path: {{ $path }} + {{- end }} + {{- end }} +{{- end }} +{{- end -}} + +{{/* +Absolute path the theme volume is mounted at, and the value of +THEME_DIRECTORY_PATH. A constant: the chart owns both ends. +*/}} +{{- define "authup.server.themeMountPath" -}} +/etc/authup/theme +{{- end -}} + +{{/* +Flatten a theme path into a valid ConfigMap key ("/" is not allowed in one). +The volume's items list projects it back, so the encoding never reaches the +operator. +*/}} +{{- define "authup.server.themeConfigMapKey" -}} +{{- . | replace "/" "__" -}} +{{- end -}} + +{{- define "authup.server.themeConfigMapName" -}} +{{- if .Values.server.theme.existingConfigMap -}} +{{- include "authup.tplvalues.render" (dict "value" .Values.server.theme.existingConfigMap "context" $) -}} +{{- else -}} +{{- printf "%s-theme" (include "authup.server.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +True when a theme should be mounted at all. +*/}} +{{- define "authup.server.themeMounted" -}} +{{- if and .Values.server.theme.enabled (or .Values.server.theme.files .Values.server.theme.existingConfigMap) -}} +true +{{- end -}} +{{- end -}} + +{{/* +Render-time validation. The chart fails loud rather than shipping a +silently-inert theme: the dominant failure mode of theming is a page that +looks exactly like an un-themed page. +*/}} +{{- define "authup.server.validateTheme" -}} +{{- if .Values.server.theme.enabled }} +{{- if not (or .Values.server.theme.files .Values.server.theme.existingConfigMap) }} +{{- fail "authup: server.theme.enabled requires server.theme.files or server.theme.existingConfigMap — an empty theme directory would render an un-themed page with no error." }} +{{- end }} +{{- if and .Values.server.theme.files .Values.server.theme.existingConfigMap }} +{{- fail "authup: set either server.theme.files or server.theme.existingConfigMap, not both — the existing ConfigMap would win and the inline files would be silently ignored." }} +{{- end }} +{{- range $path, $content := .Values.server.theme.files }} +{{- if hasPrefix "/" $path }} +{{- fail (printf "authup: server.theme.files key %q must be relative to the theme root." $path) }} +{{- end }} +{{- if contains ".." $path }} +{{- fail (printf "authup: server.theme.files key %q must not traverse out of the theme root." $path) }} +{{- end }} +{{- if contains "__" $path }} +{{- fail (printf "authup: server.theme.files key %q must not contain \"__\" — it is reserved for encoding the path separator into a ConfigMap key." $path) }} +{{- end }} +{{- end }} +{{- end }} +{{- if and .Values.server.theme.existingConfigMapItems (not .Values.server.theme.existingConfigMap) }} +{{- fail "authup: server.theme.existingConfigMapItems requires server.theme.existingConfigMap." }} +{{- end }} +{{- end -}} + {{- define "authup.server.provisioningConfigMapName" -}} {{- if .Values.server.provisioning.existingConfigMap -}} {{- include "authup.tplvalues.render" (dict "value" .Values.server.provisioning.existingConfigMap "context" $) -}} diff --git a/charts/authup/templates/server/configmap-theme.yaml b/charts/authup/templates/server/configmap-theme.yaml new file mode 100644 index 0000000..59d7169 --- /dev/null +++ b/charts/authup/templates/server/configmap-theme.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.server.enabled .Values.server.theme.enabled .Values.server.theme.files (not .Values.server.theme.existingConfigMap) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-theme" (include "authup.server.fullname" .) }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "server") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +data: + {{- /* A ConfigMap key cannot contain "/", but the theme layout has + subdirectories. Keys are flattened here and projected back to their + real path by the volume's items list, so the operator writes + "assets/theme.css" and never sees the encoding. */}} + {{- range $path, $content := .Values.server.theme.files }} + {{ include "authup.server.themeConfigMapKey" $path }}: |- + {{- include "authup.tplvalues.render" (dict "value" $content "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index 8b60e9d..e2154d7 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -40,6 +40,9 @@ spec: {{- if and .Values.server.provisioning.enabled .Values.server.provisioning.files }} checksum/provisioning: {{ include (print $.Template.BasePath "/server/configmap-provisioning.yaml") . | sha256sum }} {{- end }} + {{- if and .Values.server.theme.enabled .Values.server.theme.files }} + checksum/theme: {{ include (print $.Template.BasePath "/server/configmap-theme.yaml") . | sha256sum }} + {{- end }} {{- if .Values.server.configuration }} checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} {{- end }} @@ -158,6 +161,9 @@ spec: lifecycle: {{- include "authup.tplvalues.render" (dict "value" .Values.server.lifecycleHooks "context" $) | nindent 12 }} {{- end }} volumeMounts: {{- include "authup.server.volumeMounts" . | nindent 12 }} + {{- if include "authup.server.themeMounted" . }} + {{- include "authup.server.themeVolumeMounts" . | nindent 12 }} + {{- end }} {{- if .Values.server.extraVolumeMounts }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumeMounts "context" $) | nindent 12 }} {{- end }} @@ -165,6 +171,9 @@ spec: {{- include "authup.tplvalues.render" (dict "value" .Values.server.sidecars "context" $) | nindent 8 }} {{- end }} volumes: {{- include "authup.server.volumes" . | nindent 8 }} + {{- if include "authup.server.themeMounted" . }} + {{- include "authup.server.themeVolumes" . | nindent 8 }} + {{- end }} {{- if .Values.server.extraVolumes }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumes "context" $) | nindent 8 }} {{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 1d15b12..2143ba7 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -3306,6 +3306,55 @@ "title": "terminationGracePeriodSeconds", "type": "integer" }, + "theme": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": false, + "description": "Mount an operator theme for the served consoles (the auth console and\nthe account console). Requires an authup image that supports\nTHEME_DIRECTORY_PATH; older images ignore it", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "existingConfigMap": { + "default": "", + "description": "Existing ConfigMap holding the theme (tpl-rendered name). Use for\nbinary assets, which cannot be expressed in files", + "required": [], + "title": "existingConfigMap", + "type": "string" + }, + "existingConfigMapItems": { + "description": "Key -\u003e path projection for existingConfigMap, so its keys can land in\nsubdirectories (e.g. [{key: theme-css, path: assets/theme.css}]).\nEmpty mounts every key flat at the theme root", + "items": { + "required": [] + }, + "required": [], + "title": "existingConfigMapItems", + "type": "array" + }, + "files": { + "additionalProperties": true, + "description": "Map of path -\u003e file content, relative to the theme root (tpl-rendered).\nKeys may carry a \"/\" (\"assets/theme.css\") and are projected into\nsubdirectories. Only assets/ is served over HTTP. Text only — use\nexistingConfigMap with binaryData for images", + "required": [], + "title": "files" + }, + "fragmentsEnabled": { + "default": false, + "description": "Read fragments/head.html and splice it into the console \u003chead\u003e. Raw,\nunsanitized markup on the identity provider origin, so it is opt-in", + "required": [], + "title": "fragmentsEnabled", + "type": "boolean" + } + }, + "required": [ + "enabled", + "existingConfigMap", + "existingConfigMapItems", + "fragmentsEnabled" + ], + "title": "theme", + "type": "object" + }, "tolerations": { "description": "Tolerations", "items": { @@ -3376,6 +3425,7 @@ "configuration", "existingConfigmap", "provisioning", + "theme", "migration", "command", "args", diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 2cab0e8..5917134 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -451,6 +451,29 @@ server: # -- Existing Secret with provisioning files (tpl-rendered; takes # precedence — use for provisioning content that carries credentials) existingSecret: "" + theme: + # -- Mount an operator theme for the served consoles (the auth console and + # the account console). Requires an authup image that supports + # THEME_DIRECTORY_PATH; older images ignore it + enabled: false + # @schema + # additionalProperties: true + # @schema + # -- Map of path -> file content, relative to the theme root (tpl-rendered). + # Keys may carry a "/" ("assets/theme.css") and are projected into + # subdirectories. Only assets/ is served over HTTP. Text only — use + # existingConfigMap with binaryData for images + files: {} + # -- Existing ConfigMap holding the theme (tpl-rendered name). Use for + # binary assets, which cannot be expressed in files + existingConfigMap: "" + # -- Key -> path projection for existingConfigMap, so its keys can land in + # subdirectories (e.g. [{key: theme-css, path: assets/theme.css}]). + # Empty mounts every key flat at the theme root + existingConfigMapItems: [] + # -- Read fragments/head.html and splice it into the console . Raw, + # unsanitized markup on the identity provider origin, so it is opt-in + fragmentsEnabled: false migration: # -- Run `server/core migration run` as a pre-upgrade hook Job. Recommended # for multi-replica deployments (serializes DDL before pods roll). Fresh From 548b72533d41645de94875ff1ef9e5be0fae22d5 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 4 Aug 2026 09:36:19 +0200 Subject: [PATCH 2/3] fix(authup): reject invalid theme file keys and scope the theme env Two review findings on #5. The path -> ConfigMap key flattening only replaced "/", so a key carrying a space, a colon, an "@" or a leading dot produced a data key outside Kubernetes' ^[A-Za-z0-9._-]+$ and failed at apply time with a validation error rather than at render time. An empty key did the same. The path is now checked against the character set the server's own asset handler accepts, so the chart rejects at render time exactly what the server would 404 at request time. THEME_DIRECTORY_PATH / THEME_FRAGMENTS_ENABLED moved out of authup.server.configEnv into a deployment-only block, for the same reason the theme volume is not in authup.server.volumes: the migration Job inlines configEnv, so it was being told about a theme directory it deliberately does not mount. Nothing reads it there today (the migration command builds only the config and logger modules, never the http one), so this is consistency rather than a live failure, but the env should not describe a pod that does not exist. THEME_* stays in configEnv's reserved-key list, so a `server.config` entry still cannot emit a duplicate key into the same ConfigMap. --- charts/authup/ci/theme-values.yaml | 2 +- charts/authup/templates/_server-env.tpl | 31 ++++++++++++++++--- .../templates/server/configmap-env.yaml | 6 ++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/charts/authup/ci/theme-values.yaml b/charts/authup/ci/theme-values.yaml index 344abf0..e44d962 100644 --- a/charts/authup/ci/theme-values.yaml +++ b/charts/authup/ci/theme-values.yaml @@ -24,5 +24,5 @@ server: fragments/head.html: | -ui: +adminConsole: enabled: false diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index 5d84557..d5e71db 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -28,10 +28,6 @@ PASSWORD_RECOVERY_ENABLED: {{ .Values.server.features.passwordRecovery | toStrin EMAIL_VERIFICATION_ENABLED: {{ .Values.server.features.emailVerification | toString | quote }} MFA_ENABLED: {{ .Values.server.mfa.enabled | toString | quote }} MFA_REQUIRED: {{ .Values.server.mfa.required | toString | quote }} -{{- if include "authup.server.themeMounted" . }} -THEME_DIRECTORY_PATH: {{ include "authup.server.themeMountPath" . | quote }} -THEME_FRAGMENTS_ENABLED: {{ .Values.server.theme.fragmentsEnabled | toString | quote }} -{{- end }} {{- if .Values.auth.adminPasswordReset }} USER_ADMIN_PASSWORD_RESET: "true" {{- end }} @@ -150,6 +146,24 @@ resources, so on the upgrade that first enables theming it would reference a ConfigMap that does not exist yet and hang. A migration run has no use for the theme either way. */}} +{{/* +Theme environment, kept OUT of authup.server.configEnv for the same reason +as the volume: the migration Job inlines configEnv, and pointing +THEME_DIRECTORY_PATH at a directory that Job does not mount would describe +a pod that does not exist. Nothing reads it there today (the migration +command boots only config + logger, never the http module), but the env +should not contradict the pod it is in. + +THEME_* stays in configEnv's reserved-key list regardless, so a +`server.config` entry cannot emit a duplicate key into the same ConfigMap. +*/}} +{{- define "authup.server.themeEnv" -}} +{{- if include "authup.server.themeMounted" . }} +THEME_DIRECTORY_PATH: {{ include "authup.server.themeMountPath" . | quote }} +THEME_FRAGMENTS_ENABLED: {{ .Values.server.theme.fragmentsEnabled | toString | quote }} +{{- end }} +{{- end -}} + {{- define "authup.server.themeVolumeMounts" -}} {{- if include "authup.server.themeMounted" . }} - name: theme @@ -237,6 +251,15 @@ looks exactly like an un-themed page. {{- if contains "__" $path }} {{- fail (printf "authup: server.theme.files key %q must not contain \"__\" — it is reserved for encoding the path separator into a ConfigMap key." $path) }} {{- end }} +{{- /* A ConfigMap data key must match ^[A-Za-z0-9._-]+$, so a path + carrying a space, a colon or any other character outside this set + would flatten into an INVALID key and fail at apply time with a + Kubernetes validation error instead of here. The set is the one the + server's own asset handler accepts, so the chart now rejects at + render time exactly what the server would 404 at request time. */}} +{{- if not (regexMatch "^[a-zA-Z0-9][a-zA-Z0-9._/-]*$" $path) }} +{{- fail (printf "authup: server.theme.files key %q must start with a letter or digit and contain only letters, digits, \".\", \"_\", \"-\" and \"/\"." $path) }} +{{- end }} {{- end }} {{- end }} {{- if and .Values.server.theme.existingConfigMapItems (not .Values.server.theme.existingConfigMap) }} diff --git a/charts/authup/templates/server/configmap-env.yaml b/charts/authup/templates/server/configmap-env.yaml index 4607efe..27176cd 100644 --- a/charts/authup/templates/server/configmap-env.yaml +++ b/charts/authup/templates/server/configmap-env.yaml @@ -7,4 +7,10 @@ metadata: labels: {{- include "authup.labels" (dict "context" $ "component" "server") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} data: {{- include "authup.server.configEnv" . | nindent 2 }} + {{- /* Only the Deployment consumes this ConfigMap (envFrom); the + migration Job inlines configEnv instead, so it never sees the + theme env. See authup.server.themeEnv. */}} + {{- if include "authup.server.themeMounted" . }} + {{- include "authup.server.themeEnv" . | nindent 2 }} + {{- end }} {{- end }} From d4cdde2c25987955e987807e3396cd20ce9f471a Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 4 Aug 2026 10:01:28 +0200 Subject: [PATCH 3/3] fix(authup): include the theme env in the server checksum THEME_* moved out of authup.server.configEnv so the migration Job would not inherit env for a volume it does not mount. That also dropped it out of checksum/env, which hashes configEnv alone: flipping server.theme.fragmentsEnabled rewrote the server env ConfigMap without rolling the pods, and envFrom is snapshotted at container start, so running pods kept the stale value indefinitely. The annotation now hashes both halves of that ConfigMap. With the theme disabled the hash is unchanged, so non-theme releases do not roll on upgrade. --- charts/authup/templates/server/deployment.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index e2154d7..684b648 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -24,7 +24,11 @@ spec: {{- end }} annotations: {{- if not .Values.server.disableRestartOnChanges }} - checksum/env: {{ include "authup.server.configEnv" . | sha256sum }} + {{- /* Both halves of the env ConfigMap: themeEnv lives outside + configEnv (the migration Job must not inherit it), so hashing + configEnv alone would leave a fragmentsEnabled flip invisible + to the running pods. */}} + checksum/env: {{ printf "%s%s" (include "authup.server.configEnv" .) (include "authup.server.themeEnv" .) | sha256sum }} {{- if include "authup.auth.createSecret" . }} checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- end }}