From 12a4eca74d38bb6580a4eab9c070afcb3706b421 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 23 Aug 2026 13:30:49 +0200 Subject: [PATCH 1/5] fix(authup)!: move the writable directory to /var/lib/authup and set the env explicitly The image default moved from /usr/src/app/writable to /var/lib/authup in v1.0.0-beta.63 (authup/authup#3474). The chart mounted the old path and set no WRITABLE_DIRECTORY_PATH of its own, so the appVersion bump would have pointed server-core at a directory nothing is mounted at: production log files on the container layer, and file provisioning scanning a path that does not exist, with nothing in the log to say the directory it scanned was not there. Mount /var/lib/authup and pin WRITABLE_DIRECTORY_PATH to it rather than inheriting the image default, so the chart keeps working with an older pinned image.tag as well. server.config.WRITABLE_DIRECTORY_PATH now collides with a first-class value and fails the render. A duplicate ConfigMap key would have reintroduced the same silent mismatch, so the message names server.extraEnvVars plus a matching server.extraVolumeMounts instead, which is the only way to move a directory the chart also has to mount. BREAKING CHANGE: the writable directory moves from /usr/src/app/writable to /var/lib/authup, and server.config.WRITABLE_DIRECTORY_PATH now fails the render instead of being honored. A server.extraVolumeMounts entry aimed at the old path no longer overlays the writable directory. Migration: charts/authup/BREAKING.md. Closes #9 --- .agents/references/authup.md | 2 +- .agents/testing.md | 1 + DESIGN.md | 2 +- charts/authup/BREAKING.md | 18 ++++++++++++++++++ charts/authup/Chart.yaml | 6 +----- charts/authup/README.md | 2 +- charts/authup/templates/_server-env.tpl | 13 +++++++++---- charts/authup/values.schema.json | 2 +- charts/authup/values.yaml | 2 +- 9 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.agents/references/authup.md b/.agents/references/authup.md index 6e9639d..1691500 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -11,7 +11,7 @@ v1.0.0-beta.62 line (chart `appVersion`). |---|---|---| | One image `authup/authup`, arg-dispatched entrypoint (`server/core start`, `client/admin-console start`, `server/core migration run`, `server/core healthcheck`) | `Dockerfile`, `entrypoint.sh` (repo root) | `args` in `templates/{server,admin-console}/deployment.yaml`, `server/migration-job.yaml` | | Entrypoint force-exports `PORT=3000` / `NUXT_PORT=3000` (chart-set PORT is dead) | `entrypoint.sh` | containerPort pinned 3000 everywhere | -| Image runs as root; writable paths `/usr/src/app/writable` + npm cache | `Dockerfile` (`WRITABLE_DIRECTORY_PATH`, no `USER`) | emptyDir mounts + `npm_config_cache=/tmp/.npm-cache`; root securityContext default | +| Image runs as root; writable directory `/var/lib/authup` (moved there from `/usr/src/app/writable` in v1.0.0-beta.63, authup/authup#3474) + npm cache. The CODE default is still `/writable`, so only the image sets the FHS path | `Dockerfile` (`WRITABLE_DIRECTORY_PATH`, no `USER`), `apps/server-core/src/app/modules/config/read/env.ts` | emptyDir mounts + `npm_config_cache=/tmp/.npm-cache`; root securityContext default. The chart SETS `WRITABLE_DIRECTORY_PATH` to the path it mounts instead of inheriting the image default, so it works on either side of that bump and with a pinned older `image.tag` | | `latest`/``/`beta`/`next` tags | `.github/workflows/release.yml`, `docker-nightly.yml` | `image.tag` defaults to `Chart.AppVersion` | | `authup` CLI supervisor NOT routable through the entrypoint | `entrypoint.sh` case statement | chart never offers a combined pod | | An unknown service arg EXITS 1 since beta.59 (it used to exit 0 and start nothing); `client/web` was renamed `client/admin-console` with no alias | `entrypoint.sh` `*)` branch | chart already passes `client/admin-console` | diff --git a/.agents/testing.md b/.agents/testing.md index dbe2769..d4ea183 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -46,6 +46,7 @@ helm template t charts/authup --set server.publicUrl=auth.example.com # s helm template t charts/authup --set postgresql.enabled=false --set externalDatabase.host=db # extdb w/o password helm template t charts/authup --set server.ingress.enabled=true # ingress w/o hostname helm template t charts/authup --set server.config.PUBLIC_URL=http://x # first-class collision +helm template t charts/authup --set server.config.WRITABLE_DIRECTORY_PATH=/x # ditto; the chart pins this one to the path it mounts helm template t charts/authup --set server.theme.enabled=true # theme with no carrier helm template t charts/authup --set server.theme.enabled=true --set server.theme.title=X --set server.theme.existingConfigMap=cm # manifest + existing CM helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=logo.svg # asset outside assets/ diff --git a/DESIGN.md b/DESIGN.md index f9dfbe7..0632940 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -349,7 +349,7 @@ fail-closed semantics (an invalid file aborts boot) and camelCase keys. - Hardened `podSecurityContext` / `containerSecurityContext` defaults (runAsNonRoot 1000, readOnlyRootFilesystem, drop ALL, seccompRuntimeDefault) — with the caveat that the upstream image runs as root and `npm` wants a writable - HOME: the chart mounts emptyDirs at `/usr/src/app/writable` and `/tmp`, sets + HOME: the chart mounts emptyDirs at `/var/lib/authup` and `/tmp`, sets `npm_config_cache=/tmp/.npm-cache`, and documents that full hardening is best-effort until upstream ships a non-root image (tracked as an upstream issue). - `/metrics` is unauthenticated: `metrics.serviceMonitor` targets the Service diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index a505ac0..757462c 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -3,6 +3,24 @@ This chart uses `0.major.minor` versioning while below 1.0.0: breaking changes land on the middle digit. Every entry lists the value migrations required. +## Next release (unreleased) + +- The writable directory moves from `/usr/src/app/writable` to `/var/lib/authup`, + following the image (authup/authup#3474, shipped in v1.0.0-beta.63). The chart + mounts an emptyDir there, so nothing persists across the change; only a + `server.extraVolumeMounts` / `server.extraVolumes` entry aimed at the old path + needs updating, along with anything reading the container's log files by path. +- The chart now SETS `WRITABLE_DIRECTORY_PATH` to the path it mounts instead of + inheriting the image default, so it works with a pinned older `image.tag` too. + As a consequence `server.config.WRITABLE_DIRECTORY_PATH` now fails the render: + it would have emitted a duplicate ConfigMap key and pointed the server at a + path the chart mounts nothing at, which fails silently (production logs on the + container layer, file provisioning scanning a directory that does not exist). + To move the directory anyway, set it through `server.extraEnvVars` and mount + the same path with `server.extraVolumeMounts`; `server.provisioning` then needs + its ConfigMap re-mounted at `/provisioning` by hand, because the + chart's own provisioning mount stays where the chart puts it. + ## 0.2.0 (unreleased) Follows the upstream rename of the admin UI app (authup/authup#3370) and its diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index 6b29fe2..d82ccca 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -32,8 +32,4 @@ annotations: url: https://github.com/authup/helm artifacthub.io/changes: | - kind: fixed - description: A parent chart's own global.* keys no longer fail schema validation - - kind: added - description: server.route / adminConsole.route accept matches and filters - - kind: added - description: An HTTPRoute for a sub-path public URL without matches now fails the render + description: The writable directory moves to /var/lib/authup and WRITABLE_DIRECTORY_PATH is set to the path the chart mounts, so an image default change cannot silently stop file provisioning diff --git a/charts/authup/README.md b/charts/authup/README.md index 16673d3..1d1e318 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -349,7 +349,7 @@ Kubernetes: `>=1.25.0-0` | server.command | list | `[]` | Override the container command | | server.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap (map of NAME: value) for options without first-class values, e.g. AUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console with your own build (pair them with extraVolumes; the substituted package owns the login flow, so use server.theme for branding instead) | | server.configuration | string | `""` | Content of an authup.server.core.conf mounted into the working directory for file-only options (middleware objects, per-field SMTP, CORS allowlist). Environment variables always win over file values. | -| server.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context. The upstream image runs as root and needs a writable npm cache; the chart mounts emptyDirs at /usr/src/app/writable and /tmp to keep readOnlyRootFilesystem viable. | +| server.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context. The upstream image runs as root and needs a writable npm cache; the chart mounts emptyDirs at /var/lib/authup and /tmp to keep readOnlyRootFilesystem viable. | | server.customLivenessProbe | object | `{}` | Custom liveness probe | | server.customReadinessProbe | object | `{}` | Custom readiness probe | | server.customStartupProbe | object | `{}` | Custom startup probe overriding the structured one | diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index 38ce47c..615628f 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -38,10 +38,15 @@ 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" "ACCOUNT_CONSOLE_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" }} +{{- /* Pinned to the path the volumeMounts use, not inherited from the image, whose default + moved here in v1.0.0-beta.63: a mount that stops matching it fails silently (logs on + the container layer, file provisioning scanning a directory that is not there). */}} +WRITABLE_DIRECTORY_PATH: "/var/lib/authup" +{{- $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" "ACCOUNT_CONSOLE_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "WRITABLE_DIRECTORY_PATH" "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) }} +{{- $instead := ternary "server.extraEnvVars plus a matching server.extraVolumeMounts" "the dedicated value" (eq $key "WRITABLE_DIRECTORY_PATH") }} +{{- fail (printf "authup: server.config.%s collides with a first-class chart value — set it through %s instead." $key $instead) }} {{- end }} {{ $key }}: {{ include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $) | quote }} {{- end }} @@ -102,12 +107,12 @@ provisioning files, config file). */}} {{- define "authup.server.volumeMounts" -}} - name: writable - mountPath: /usr/src/app/writable + mountPath: /var/lib/authup - name: tmp mountPath: /tmp {{- if and .Values.server.provisioning.enabled (or .Values.server.provisioning.files .Values.server.provisioning.existingConfigMap .Values.server.provisioning.existingSecret) }} - name: provisioning - mountPath: /usr/src/app/writable/provisioning + mountPath: /var/lib/authup/provisioning readOnly: true {{- end }} {{- if or .Values.server.configuration .Values.server.existingConfigmap }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 608aa7f..c10e425 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -2233,7 +2233,7 @@ }, "containerSecurityContext": { "additionalProperties": true, - "description": "Container security context. The upstream image runs as root and needs a\nwritable npm cache; the chart mounts emptyDirs at /usr/src/app/writable and\n/tmp to keep readOnlyRootFilesystem viable.", + "description": "Container security context. The upstream image runs as root and needs a\nwritable npm cache; the chart mounts emptyDirs at /var/lib/authup and\n/tmp to keep readOnlyRootFilesystem viable.", "properties": { "allowPrivilegeEscalation": { "default": false, diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 3fe6741..3417631 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -595,7 +595,7 @@ server: # additionalProperties: true # @schema # -- Container security context. The upstream image runs as root and needs a - # writable npm cache; the chart mounts emptyDirs at /usr/src/app/writable and + # writable npm cache; the chart mounts emptyDirs at /var/lib/authup and # /tmp to keep readOnlyRootFilesystem viable. containerSecurityContext: enabled: true From be07ca7e61bfaa604b32e224d9170bdd5757ccc8 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 23 Aug 2026 09:41:12 +0200 Subject: [PATCH 2/5] feat(authup): accept a tpl-rendered route.enabled Every other field of server.route and adminConsole.route is tpl-rendered, but enabled was typed boolean in values.schema.json, so a template string was rejected before rendering. An umbrella chart bundling authup behind one Gateway API switch could express the whole route through values, matches and filters included, and still not tie its existence to that switch. Widen the schema to [boolean, string] and read the value through a new authup.flag helper at all six read sites: both HTTPRoutes, both sub-path catch-all validations and both NOTES warnings. Converting fewer would leave the guard of invariant 18 reading the raw value, i.e. disarmed for exactly the umbrella users this is for. The helper is strict on purpose. The widened schema no longer rejects garbage, and a template rendering to "false" is a non-empty and therefore truthy string, so a plain if would create the route precisely when the parent switched it off. Only true and false (or a template rendering to one of them) are accepted; anything else fails the render naming the key. ci/default-values.yaml carries the false direction as the in-repo regression guard: it costs no install scenario, and the kind cluster has no Gateway API CRDs to render a route against. Closes #15 --- .agents/architecture.md | 7 +++++++ .agents/testing.md | 19 +++++++++++++++++++ charts/authup/Chart.yaml | 2 ++ charts/authup/README.md | 4 ++-- charts/authup/ci/default-values.yaml | 6 ++++++ charts/authup/templates/NOTES.txt | 4 ++-- charts/authup/templates/_helpers.tpl | 19 +++++++++++++++++++ .../templates/admin-console/httproute.yaml | 2 +- charts/authup/templates/server/httproute.yaml | 2 +- charts/authup/templates/validations.yaml | 4 ++-- charts/authup/values.schema.json | 14 ++++++++++---- charts/authup/values.yaml | 14 ++++++++++++-- 12 files changed, 83 insertions(+), 14 deletions(-) diff --git a/.agents/architecture.md b/.agents/architecture.md index 92acbb2..44f11e2 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -91,6 +91,13 @@ editing templates or values. `authup.tplvalues.render`, so umbrella charts can inject template expressions (the PrivateAIM lesson: their untemplatable `existingSecret` forced a hardcoded-names table). + `server.route.enabled` / `adminConsole.route.enabled` extend this to a + BOOLEAN, read through `authup.flag`. That reader is strict by necessity: + the schema is widened to `[boolean, string]` so it no longer rejects + garbage, and a rendered `"false"` is a non-empty (truthy) string, so a + plain `if` would create the route exactly when the parent switched it off. + All six read sites (2 HTTPRoutes, 2 validations, 2 NOTES) convert together + or the sub-path catch-all guard of rule 18 stops covering umbrella users. 17. **`global` must stay open in the schema.** helm copies a parent chart's ENTIRE `global` map into every subchart before validating that subchart's schema, so `additionalProperties: false` there makes the chart diff --git a/.agents/testing.md b/.agents/testing.md index d4ea183..a904161 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -47,6 +47,7 @@ helm template t charts/authup --set postgresql.enabled=false --set externalDatab helm template t charts/authup --set server.ingress.enabled=true # ingress w/o hostname helm template t charts/authup --set server.config.PUBLIC_URL=http://x # first-class collision helm template t charts/authup --set server.config.WRITABLE_DIRECTORY_PATH=/x # ditto; the chart pins this one to the path it mounts +helm template t charts/authup --set 'server.route.enabled=yes' # flag that is neither true nor false helm template t charts/authup --set server.theme.enabled=true # theme with no carrier helm template t charts/authup --set server.theme.enabled=true --set server.theme.title=X --set server.theme.existingConfigMap=cm # manifest + existing CM helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=logo.svg # asset outside assets/ @@ -63,6 +64,24 @@ case needs its own line: only the origin reaches the HTTPRoute hostname, and the dropped path is exactly what turns the rule into a catch-all. Adding `--set 'server.route.matches[0].path.value=/auth'` must make both RENDER. +`server.route.enabled` / `adminConsole.route.enabled` accept a tpl-rendered +string, so an umbrella can drive them from one of its own switches. `--set-string` +cannot carry `{{ }}` (helm fails parsing on the closing brace), so both directions +go through a values file, and BOTH are needed: a rendered `"false"` is a non-empty +string, which a Go template `if` reads as true. + +```bash +printf 'global:\n gw:\n enabled: false\nserver:\n route:\n enabled: "{{ .Values.global.gw.enabled }}"\n' \ + | helm template t charts/authup -f - | grep -c 'kind: HTTPRoute' # must be 0 +printf 'global:\n gw:\n enabled: true\nserver:\n publicUrl: https://auth.example.com\n route:\n enabled: "{{ .Values.global.gw.enabled }}"\n parentRefs:\n - name: gw\n' \ + | helm template t charts/authup -f - | grep -c 'kind: HTTPRoute' # must be 1 +``` + +The sub-path catch-all guard and the NOTES path-prefix warning read the same +flag, so all six read sites convert together: leave one raw and an umbrella-driven +route renders unguarded. `ci/default-values.yaml` carries the false direction as +the in-repo regression guard. + Umbrella use is part of the contract: `global` must stay open. Render a throwaway parent chart with authup in `charts/` and an unrelated global (`global.myOrgKey`) whenever the schema generation changes; `ci/default-values.yaml` carries a stray diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index d82ccca..45febbf 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -33,3 +33,5 @@ annotations: artifacthub.io/changes: | - kind: fixed description: The writable directory moves to /var/lib/authup and WRITABLE_DIRECTORY_PATH is set to the path the chart mounts, so an image default change cannot silently stop file provisioning + - kind: added + description: server.route.enabled / adminConsole.route.enabled accept a tpl-rendered string, so an umbrella chart can drive them from a global diff --git a/charts/authup/README.md b/charts/authup/README.md index 1d1e318..77151ae 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -230,7 +230,7 @@ Kubernetes: `>=1.25.0-0` | adminConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | UI container resources | | adminConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | | adminConsole.route.annotations | object | `{}` | HTTPRoute annotations | -| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the UI | +| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches) | | adminConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | | adminConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) | | adminConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | @@ -434,7 +434,7 @@ Kubernetes: `>=1.25.0-0` | server.resources | object | `{"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}}` | Server container resources | | server.revisionHistoryLimit | int | `3` | Deployment revision history limit | | server.route.annotations | object | `{}` | HTTPRoute annotations | -| server.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for server-core | +| server.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches) | | server.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | | server.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) | | server.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | diff --git a/charts/authup/ci/default-values.yaml b/charts/authup/ci/default-values.yaml index ec449d4..f77f99a 100644 --- a/charts/authup/ci/default-values.yaml +++ b/charts/authup/ci/default-values.yaml @@ -4,6 +4,10 @@ # reject globals this chart does not declare. global: umbrellaProbe: parent-injected + # A templated route.enabled rendering to "false" is the second umbrella guard: + # the schema must accept the string, and the render must produce no HTTPRoute + # (a rendered "false" is a non-empty, i.e. truthy, string to a Go template). + gatewayApiProbe: false postgresql: persistence: enabled: false @@ -17,3 +21,5 @@ adminConsole: requests: cpu: 25m memory: 128Mi + route: + enabled: '{{ .Values.global.gatewayApiProbe }}' diff --git a/charts/authup/templates/NOTES.txt b/charts/authup/templates/NOTES.txt index 19a529e..1ee7dd2 100644 --- a/charts/authup/templates/NOTES.txt +++ b/charts/authup/templates/NOTES.txt @@ -99,10 +99,10 @@ small deployments. For production, consider an external database {{- end }} {{- $prefixed := list }} -{{- if and .Values.server.enabled .Values.server.route.enabled (urlParse $serverUrl).path (ne (urlParse $serverUrl).path "/") }} +{{- if and .Values.server.enabled (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) (urlParse $serverUrl).path (ne (urlParse $serverUrl).path "/") }} {{- $prefixed = append $prefixed (printf "server.route %s" (urlParse $serverUrl).path) }} {{- end }} -{{- if and .Values.adminConsole.enabled .Values.adminConsole.route.enabled (urlParse $uiUrl).path (ne (urlParse $uiUrl).path "/") }} +{{- if and .Values.adminConsole.enabled (include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled")) (urlParse $uiUrl).path (ne (urlParse $uiUrl).path "/") }} {{- $prefixed = append $prefixed (printf "adminConsole.route %s" (urlParse $uiUrl).path) }} {{- end }} {{- if $prefixed }} diff --git a/charts/authup/templates/_helpers.tpl b/charts/authup/templates/_helpers.tpl index 9b362b4..a62bc24 100644 --- a/charts/authup/templates/_helpers.tpl +++ b/charts/authup/templates/_helpers.tpl @@ -73,6 +73,25 @@ Usage: {{ include "authup.tplvalues.render" (dict "value" .Values. "contex {{- end -}} {{- end -}} +{{/* +Boolean that may arrive as a tpl-rendered string, so an umbrella chart can drive +it from one of its own switches. Returns "true" (truthy) or "" (falsy). + +Strict on purpose. The schema no longer rejects a string here, and a rendered +"false" is a NON-EMPTY string, i.e. truthy to a Go template `if`: a lenient +reader would create the resource exactly when the parent switched it off. +Anything that is not true/false/"" fails the render instead. +Usage: {{ if (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) }} +*/}} +{{- define "authup.flag" -}} +{{- $value := include "authup.tplvalues.render" (dict "value" .value "context" .context) | trim | lower -}} +{{- if eq $value "true" -}} +true +{{- else if not (or (eq $value "false") (eq $value "")) -}} +{{- fail (printf "authup: %s must be true or false (or a template rendering to one of them), got %q." .key $value) -}} +{{- end -}} +{{- end -}} + {{/* Standard labels. Usage: {{ include "authup.labels" (dict "context" $ "component" "server") }} diff --git a/charts/authup/templates/admin-console/httproute.yaml b/charts/authup/templates/admin-console/httproute.yaml index f3a4916..4eb249d 100644 --- a/charts/authup/templates/admin-console/httproute.yaml +++ b/charts/authup/templates/admin-console/httproute.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.adminConsole.enabled .Values.adminConsole.route.enabled }} +{{- if and .Values.adminConsole.enabled (include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled")) }} {{- include "authup.httproute" (dict "context" $ "component" "admin-console" diff --git a/charts/authup/templates/server/httproute.yaml b/charts/authup/templates/server/httproute.yaml index ab7c98f..c28478e 100644 --- a/charts/authup/templates/server/httproute.yaml +++ b/charts/authup/templates/server/httproute.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.server.enabled .Values.server.route.enabled }} +{{- if and .Values.server.enabled (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) }} {{- include "authup.httproute" (dict "context" $ "component" "server" diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index ffac23c..429561c 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -57,13 +57,13 @@ derived from the public URL's ORIGIN, so a sub-path public URL would silently at authup as the catch-all backend of a hostname it is meant to share. authup always serves at "/" and expects the proxy to strip the prefix, hence match AND rewrite. */}} -{{- if and .Values.server.enabled .Values.server.route.enabled (not .Values.server.route.matches) }} +{{- if and .Values.server.enabled (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) (not .Values.server.route.matches) }} {{- $path := (urlParse (include "authup.server.publicUrl" .)).path }} {{- if and $path (ne $path "/") }} {{- fail (printf "authup: the server public URL carries the path %q but server.route.matches is empty, which the Gateway API defaults to PathPrefix \"/\" (a catch-all on every hostname this route attaches to). authup serves at \"/\" and expects the proxy to strip the prefix, so set both:\n server.route.matches: [{path: {type: PathPrefix, value: %s}}]\n server.route.filters: [{type: URLRewrite, urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}}]" $path $path) }} {{- end }} {{- end }} -{{- if and .Values.adminConsole.enabled .Values.adminConsole.route.enabled (not .Values.adminConsole.route.matches) }} +{{- if and .Values.adminConsole.enabled (include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled")) (not .Values.adminConsole.route.matches) }} {{- $path := (urlParse (include "authup.adminConsole.publicUrl" .)).path }} {{- if and $path (ne $path "/") }} {{- fail (printf "authup: the admin console public URL carries the path %q but adminConsole.route.matches is empty, which the Gateway API defaults to PathPrefix \"/\" (a catch-all on every hostname this route attaches to). Set both:\n adminConsole.route.matches: [{path: {type: PathPrefix, value: %s}}]\n adminConsole.route.filters: [{type: URLRewrite, urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}}]" $path $path) }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index c10e425..ae5d857 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -737,10 +737,13 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for the UI", + "description": "Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches)", "required": [], "title": "enabled", - "type": "boolean" + "type": [ + "boolean", + "string" + ] }, "filters": { "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", @@ -3110,10 +3113,13 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for server-core", + "description": "Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches)", "required": [], "title": "enabled", - "type": "boolean" + "type": [ + "boolean", + "string" + ] }, "filters": { "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 3417631..393b007 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -746,7 +746,12 @@ server: # -- Full custom rules (tpl-rendered; appended after the generated rules) extraRules: [] route: - # -- Create a Gateway API HTTPRoute for server-core + # @schema + # type: [boolean, string] + # required: true + # @schema + # -- Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to + # "true" enables it, so an umbrella chart can drive this from one of its own switches) enabled: false # -- Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public # URL path is dropped and needs its own matches entry) @@ -1052,7 +1057,12 @@ adminConsole: # -- Full custom rules (tpl-rendered; appended after the generated rules) extraRules: [] route: - # -- Create a Gateway API HTTPRoute for the UI + # @schema + # type: [boolean, string] + # required: true + # @schema + # -- Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to + # "true" enables it, so an umbrella chart can drive this from one of its own switches) enabled: false # -- Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public # URL path is dropped and needs its own matches entry) From 85d795f3b87b94340a1cbdddddfe0eda4ed0d29e Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 23 Aug 2026 09:48:26 +0200 Subject: [PATCH 3/5] docs(authup): name both provisioning source types in the migration note server.provisioning also accepts existingSecret, which mounts as a Secret, so the WRITABLE_DIRECTORY_PATH migration note applies to it equally. --- charts/authup/BREAKING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index 757462c..3bd34a2 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -18,8 +18,8 @@ land on the middle digit. Every entry lists the value migrations required. container layer, file provisioning scanning a directory that does not exist). To move the directory anyway, set it through `server.extraEnvVars` and mount the same path with `server.extraVolumeMounts`; `server.provisioning` then needs - its ConfigMap re-mounted at `/provisioning` by hand, because the - chart's own provisioning mount stays where the chart puts it. + its source (ConfigMap or Secret) re-mounted at `/provisioning` by + hand, because the chart's own provisioning mount stays where the chart puts it. ## 0.2.0 (unreleased) From d4399789a336df2d9875328270ef46e624eaaea6 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 23 Aug 2026 12:57:43 +0200 Subject: [PATCH 4/5] feat(authup): track authup 1.0.0-beta.63 The only image-contract change between beta.62 and beta.63 is the writable directory move the previous commit already followed: entrypoint.sh is unchanged, so the arg dispatch, the forced PORT=3000 and every env name the chart sets still hold. --- .agents/references/authup.md | 2 +- charts/authup/Chart.yaml | 4 +++- charts/authup/README.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.agents/references/authup.md b/.agents/references/authup.md index 1691500..59b5118 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -3,7 +3,7 @@ Repo: https://github.com/authup/authup (local checkout commonly at `/opt/projects/authup/authup`). The chart encodes facts about the app; verify against these sources when authup releases change behavior. Pinned against the -v1.0.0-beta.62 line (chart `appVersion`). +v1.0.0-beta.63 line (chart `appVersion`). ## Image / entrypoint contract diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index 45febbf..f37db84 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -5,7 +5,7 @@ description: Authup is an authentication & authorization system. This chart with optional built-in PostgreSQL, MySQL and Valkey instances. type: application version: 0.2.2 -appVersion: "1.0.0-beta.62" +appVersion: "1.0.0-beta.63" kubeVersion: ">=1.25.0-0" home: https://authup.org icon: https://raw.githubusercontent.com/authup/helm/master/assets/icon.svg @@ -33,5 +33,7 @@ annotations: artifacthub.io/changes: | - kind: fixed description: The writable directory moves to /var/lib/authup and WRITABLE_DIRECTORY_PATH is set to the path the chart mounts, so an image default change cannot silently stop file provisioning + - kind: changed + description: Track authup v1.0.0-beta.63, whose image writes to /var/lib/authup - kind: added description: server.route.enabled / adminConsole.route.enabled accept a tpl-rendered string, so an umbrella chart can drive them from a global diff --git a/charts/authup/README.md b/charts/authup/README.md index 77151ae..e63d31a 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -7,7 +7,7 @@ # authup ![Version](https://img.shields.io/badge/Version-0.2.2?style=flat-square&color=informational) -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.62](https://img.shields.io/badge/AppVersion-1.0.0--beta.62-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.63](https://img.shields.io/badge/AppVersion-1.0.0--beta.63-informational?style=flat-square) Authup is an authentication & authorization system. This chart deploys the server-core IdP/API service and the client-admin-console admin UI, with optional built-in PostgreSQL, MySQL and Valkey instances. It deploys: From 0833b5cf82bdcf4cde0b525de2b7c11072c3121e Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 23 Aug 2026 13:30:32 +0200 Subject: [PATCH 5/5] fix(authup): validate the route flag unconditionally and quote the value the user wrote Three follow-ups from auditing the two previous commits. The render gates spell `and .enabled (include "authup.flag" ...)` and Go short-circuits `and`, so a disabled component never reached the helper. The schema rejected a malformed value either way before it was widened to [boolean, string], so that was a regression: validations.yaml now runs both flags unconditionally, which is where the chart keeps its render-nothing guards. authup.flag lower-cased the value before quoting it into the failure message, so the one string the error handed the operator to search for was not the one in their values file (a stray template reported `.values`, `Yes` reported `yes`). Lower only for the comparison. values.yaml documented the accepting half of the contract and not the strict half. Say that "false" and "" disable and anything else fails the render. --- .agents/testing.md | 1 + charts/authup/README.md | 4 ++-- charts/authup/templates/_helpers.tpl | 5 +++-- charts/authup/templates/validations.yaml | 10 ++++++++++ charts/authup/values.schema.json | 4 ++-- charts/authup/values.yaml | 6 ++++-- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.agents/testing.md b/.agents/testing.md index a904161..8ffaf33 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -48,6 +48,7 @@ helm template t charts/authup --set server.ingress.enabled=true # i helm template t charts/authup --set server.config.PUBLIC_URL=http://x # first-class collision helm template t charts/authup --set server.config.WRITABLE_DIRECTORY_PATH=/x # ditto; the chart pins this one to the path it mounts helm template t charts/authup --set 'server.route.enabled=yes' # flag that is neither true nor false +helm template t charts/authup --set adminConsole.enabled=false --set adminConsole.route.enabled=yes # ditto: validated even with the component off helm template t charts/authup --set server.theme.enabled=true # theme with no carrier helm template t charts/authup --set server.theme.enabled=true --set server.theme.title=X --set server.theme.existingConfigMap=cm # manifest + existing CM helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=logo.svg # asset outside assets/ diff --git a/charts/authup/README.md b/charts/authup/README.md index e63d31a..d5e6879 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -230,7 +230,7 @@ Kubernetes: `>=1.25.0-0` | adminConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | UI container resources | | adminConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | | adminConsole.route.annotations | object | `{}` | HTTPRoute annotations | -| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches) | +| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches; "false" and "" disable it, anything else fails the render) | | adminConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | | adminConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) | | adminConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | @@ -434,7 +434,7 @@ Kubernetes: `>=1.25.0-0` | server.resources | object | `{"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}}` | Server container resources | | server.revisionHistoryLimit | int | `3` | Deployment revision history limit | | server.route.annotations | object | `{}` | HTTPRoute annotations | -| server.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches) | +| server.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to "true" enables it, so an umbrella chart can drive this from one of its own switches; "false" and "" disable it, anything else fails the render) | | server.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | | server.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) | | server.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | diff --git a/charts/authup/templates/_helpers.tpl b/charts/authup/templates/_helpers.tpl index a62bc24..ad125b4 100644 --- a/charts/authup/templates/_helpers.tpl +++ b/charts/authup/templates/_helpers.tpl @@ -84,11 +84,12 @@ Anything that is not true/false/"" fails the render instead. Usage: {{ if (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) }} */}} {{- define "authup.flag" -}} -{{- $value := include "authup.tplvalues.render" (dict "value" .value "context" .context) | trim | lower -}} +{{- $rendered := include "authup.tplvalues.render" (dict "value" .value "context" .context) | trim -}} +{{- $value := lower $rendered -}} {{- if eq $value "true" -}} true {{- else if not (or (eq $value "false") (eq $value "")) -}} -{{- fail (printf "authup: %s must be true or false (or a template rendering to one of them), got %q." .key $value) -}} +{{- fail (printf "authup: %s must be true or false (or a template rendering to one of them), got %q." .key $rendered) -}} {{- end -}} {{- end -}} diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index 429561c..7e8a499 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -50,6 +50,16 @@ store, token blocklist and MFA challenges fall back to a per-process memory cach {{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname (or extraHosts / extraRules)." }} {{- end }} +{{/* +The route flags are validated here unconditionally. The render gates spell +`and .enabled (include "authup.flag" ...)`, and Go short-circuits +`and`, so a disabled component would never reach the helper: the schema used to +reject a bad value either way, and widening it to [boolean, string] moved that +job into the template. +*/}} +{{- $_ := include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled") }} +{{- $_ = include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled") }} + {{/* A Gateway API HTTPRoute rule without `matches` defaults to PathPrefix "/", i.e. it matches every request on every hostname the route attaches to. The route hostname is diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index ae5d857..7324034 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -737,7 +737,7 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches)", + "description": "Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches;\n\"false\" and \"\" disable it, anything else fails the render)", "required": [], "title": "enabled", "type": [ @@ -3113,7 +3113,7 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches)", + "description": "Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to\n\"true\" enables it, so an umbrella chart can drive this from one of its own switches;\n\"false\" and \"\" disable it, anything else fails the render)", "required": [], "title": "enabled", "type": [ diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 393b007..f205f23 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -751,7 +751,8 @@ server: # required: true # @schema # -- Create a Gateway API HTTPRoute for server-core (tpl-rendered: a string rendering to - # "true" enables it, so an umbrella chart can drive this from one of its own switches) + # "true" enables it, so an umbrella chart can drive this from one of its own switches; + # "false" and "" disable it, anything else fails the render) enabled: false # -- Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public # URL path is dropped and needs its own matches entry) @@ -1062,7 +1063,8 @@ adminConsole: # required: true # @schema # -- Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to - # "true" enables it, so an umbrella chart can drive this from one of its own switches) + # "true" enables it, so an umbrella chart can drive this from one of its own switches; + # "false" and "" disable it, anything else fails the render) enabled: false # -- Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public # URL path is dropped and needs its own matches entry)