From ec6d34ae2a2dbe0d3b0850e85f1f40e5edc601aa Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:27:00 +0200 Subject: [PATCH 01/14] feat(authup): adopt the beta.64 runtime contract --- Makefile | 6 +- charts/authup/Chart.yaml | 2 +- charts/authup/templates/_server-env.tpl | 36 +++-- .../server/configmap-configuration.yaml | 2 +- .../configmap-migration-configuration.yaml | 4 +- .../authup/templates/server/deployment.yaml | 5 +- .../templates/server/migration-job.yaml | 1 - scripts/check-beta64-contract.py | 141 ++++++++++++++++++ 8 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 scripts/check-beta64-contract.py diff --git a/Makefile b/Makefile index 1e39fab..a927f38 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ HELM_DOCS_VERSION := v1.14.2 HELM_SCHEMA_VERSION := 0.18.1 -.PHONY: docs schema lint lint-values-coverage template test +.PHONY: docs schema lint lint-values-coverage lint-beta64-contract template test ## Generate per-chart README.md from README.md.gotmpl + values.yaml comments. docs: @@ -21,6 +21,10 @@ lint: lint-values-coverage: python3 scripts/check-values-coverage.py charts/authup +## Assert the authup beta.64 process, configuration and filesystem contract. +lint-beta64-contract: + python3 scripts/check-beta64-contract.py charts/authup all + ## Render the chart with every ci values file. template: @for f in charts/authup/ci/*-values.yaml; do \ diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index 9078b96..fc9a917 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.3.0 -appVersion: "1.0.0-beta.63" +appVersion: "1.0.0-beta.64" kubeVersion: ">=1.25.0-0" home: https://authup.org icon: https://raw.githubusercontent.com/authup/helm/master/assets/icon.svg diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index eeb9fb3..ac1290c 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -26,7 +26,6 @@ TRUST_PROXY: {{ .Values.server.trustProxy | toString | quote }} REGISTRATION_ENABLED: {{ .Values.server.features.registration | toString | quote }} PASSWORD_RECOVERY_ENABLED: {{ .Values.server.features.passwordRecovery | toString | quote }} EMAIL_VERIFICATION_ENABLED: {{ .Values.server.features.emailVerification | toString | quote }} -ACCOUNT_CONSOLE_ENABLED: {{ .Values.server.features.accountConsole | toString | quote }} MFA_ENABLED: {{ .Values.server.mfa.enabled | toString | quote }} MFA_REQUIRED: {{ .Values.server.mfa.required | toString | quote }} {{- if .Values.auth.adminPasswordReset }} @@ -38,15 +37,14 @@ CLIENT_SYSTEM_ENABLED: "true" CLIENT_SYSTEM_SECRET_RESET: "true" {{- end }} {{- end }} -{{- /* 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" }} +{{- if .Values.server.provisioning.enabled }} +PROVISIONING_DIRECTORY_PATH: "/etc/authup/provisioning" +{{- end }} +LOG_DIRECTORY_PATH: "/var/log/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" "PROVISIONING_DIRECTORY_PATH" "LOG_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 }} -{{- $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) }} +{{- fail (printf "authup: server.config.%s collides with a first-class chart value — set it through the dedicated value instead." $key) }} {{- end }} {{ $key }}: {{ include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $) | quote }} {{- end }} @@ -125,12 +123,12 @@ DB_PASSWORD, without which the migration cannot run, and the KEK (see below). {{- end -}} {{/* -Shared volumes / volumeMounts for the server container (writable dir, tmp, +Shared volumes / volumeMounts for the server container (logs, tmp, provisioning files, config file). Usage: {{ include "authup.server.volumeMounts" (dict "context" $ "hook" true) }} The `required` on .context is load-bearing: helm renders with missingkey=zero, so a call site that passed a bare `.` would leave every guard below reading false and -emit writable+tmp only, silently dropping the config file. Failing the render is +emit logs+tmp only, silently dropping the config file. Failing the render is the chart's posture everywhere else. "hook" marks the pre-upgrade migration Job. It drops the provisioning mount, @@ -144,7 +142,7 @@ image's NODE_ENV=production the logger opens /http.log and uncreatable path is a hard ENOENT failure. The config file stays for both as well, and mounting it is not optional: -`migration run` loads authup.server.core.conf unconditionally, and the db keys +`migration run` loads authup.yml unconditionally, and the db keys that only the file can carry (ssl, socketPath, replication, extensions, poolSize) decide how the migration connects and what it creates. Dropping it would silently migrate over a plaintext connection. The Job reads it from a hook-scoped copy @@ -159,19 +157,19 @@ env, and THEME_* must stay in its reserved-key list either way). */}} {{- define "authup.server.volumeMounts" -}} {{- $ctx := required "authup.server.volumeMounts: call it as (dict \"context\" $ \"hook\" bool)" .context -}} -- name: writable - mountPath: /var/lib/authup +- name: logs + mountPath: /var/log/authup - name: tmp mountPath: /tmp {{- if and (not .hook) $ctx.Values.server.provisioning.enabled (or $ctx.Values.server.provisioning.files $ctx.Values.server.provisioning.existingConfigMap $ctx.Values.server.provisioning.existingSecret) }} - name: provisioning - mountPath: /var/lib/authup/provisioning + mountPath: /etc/authup/provisioning readOnly: true {{- end }} {{- if or $ctx.Values.server.configuration $ctx.Values.server.existingConfigmap }} - name: configuration - mountPath: /usr/src/app/authup.server.core.conf - subPath: authup.server.core.conf + mountPath: /etc/authup/authup.yml + subPath: authup.yml readOnly: true {{- end }} {{- if and (not .hook) (include "authup.server.themeMounted" $ctx) }} @@ -183,7 +181,7 @@ env, and THEME_* must stay in its reserved-key list either way). {{- define "authup.server.volumes" -}} {{- $ctx := required "authup.server.volumes: call it as (dict \"context\" $ \"hook\" bool)" .context -}} -- name: writable +- name: logs emptyDir: {} - name: tmp emptyDir: {} @@ -451,7 +449,7 @@ looks exactly like an un-themed page. {{- end -}} {{/* -ConfigMap carrying authup.server.core.conf for one consumer. +ConfigMap carrying authup.yml for one consumer. Usage: {{ include "authup.server.configurationConfigMapName" (dict "context" $ "hook" true) }} "hook" resolves to the migration Job's own copy (templates/server/configmap- @@ -475,7 +473,7 @@ outside the release and already exists when the hook runs. {{- end -}} {{/* -Rendered content of authup.server.core.conf. One source for the release +Rendered content of authup.yml. One source for the release ConfigMap and the hook copy, so the migration cannot run against a config file that differs from the one the server pods get. */}} diff --git a/charts/authup/templates/server/configmap-configuration.yaml b/charts/authup/templates/server/configmap-configuration.yaml index ad31b31..523673c 100644 --- a/charts/authup/templates/server/configmap-configuration.yaml +++ b/charts/authup/templates/server/configmap-configuration.yaml @@ -7,6 +7,6 @@ metadata: labels: {{- include "authup.labels" (dict "context" $ "component" "server") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} data: - authup.server.core.conf: |- + authup.yml: |- {{- include "authup.server.configurationContent" . | nindent 4 }} {{- end }} diff --git a/charts/authup/templates/server/configmap-migration-configuration.yaml b/charts/authup/templates/server/configmap-migration-configuration.yaml index ffd99de..1cd22dc 100644 --- a/charts/authup/templates/server/configmap-migration-configuration.yaml +++ b/charts/authup/templates/server/configmap-migration-configuration.yaml @@ -1,6 +1,6 @@ {{- if and .Values.server.enabled .Values.server.migration.enabled .Values.server.configuration (not .Values.server.existingConfigmap) }} {{/* -The migration Job's own copy of authup.server.core.conf. +The migration Job's own copy of authup.yml. `migration run` loads the config file unconditionally, and the db options only the file can carry (ssl, socketPath, replication, extensions, poolSize) decide @@ -34,6 +34,6 @@ metadata: argocd.argoproj.io/sync-wave: "-5" {{- end }} data: - authup.server.core.conf: |- + authup.yml: |- {{- include "authup.server.configurationContent" . | nindent 4 }} {{- end }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index 5c91523..b90560e 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -106,7 +106,6 @@ spec: args: {{- include "authup.tplvalues.render" (dict "value" .Values.server.args "context" $) | nindent 12 }} {{- else }} args: - - server/core - start {{- end }} {{- end }} @@ -122,6 +121,10 @@ spec: name: {{ include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVarsSecret "context" $) }} {{- end }} env: {{- include "authup.server.secretEnv" (dict "context" $) | nindent 12 }} + {{- if and .Values.server.migration.enabled .Release.IsUpgrade }} + - name: MIGRATION_ENABLED + value: "false" + {{- end }} {{- if .Values.server.extraEnvVars }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVars "context" $) | nindent 12 }} {{- end }} diff --git a/charts/authup/templates/server/migration-job.yaml b/charts/authup/templates/server/migration-job.yaml index 5b94308..2d159b9 100644 --- a/charts/authup/templates/server/migration-job.yaml +++ b/charts/authup/templates/server/migration-job.yaml @@ -58,7 +58,6 @@ spec: securityContext: {{- include "authup.securityContext" .Values.server.containerSecurityContext | nindent 12 }} {{- end }} args: - - server/core - migration - run {{- if or .Values.server.extraEnvVarsCM .Values.server.extraEnvVarsSecret }} diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py new file mode 100644 index 0000000..f3f5333 --- /dev/null +++ b/scripts/check-beta64-contract.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Assert the Authup beta.64 runtime contract against rendered manifests.""" + +import subprocess +import sys +import tempfile +from pathlib import Path + +import yaml + + +chart = Path(sys.argv[1] if len(sys.argv) > 1 else "charts/authup") +case = sys.argv[2] if len(sys.argv) > 2 else "all" + + +def render(values=None, *args): + command = ["helm", "template", "test", str(chart)] + if values is not None: + with tempfile.NamedTemporaryFile("w", suffix=".yaml") as handle: + yaml.safe_dump(values, handle) + handle.flush() + result = subprocess.run( + [*command, "-f", handle.name, *args], + check=True, + capture_output=True, + text=True, + ) + else: + result = subprocess.run( + [*command, *args], + check=True, + capture_output=True, + text=True, + ) + return [document for document in yaml.safe_load_all(result.stdout) if document] + + +def one(documents, kind, component, suffix=None): + matches = [ + document for document in documents + if document.get("kind") == kind + and document.get("metadata", {}).get("labels", {}).get( + "app.kubernetes.io/component" + ) == component + and (suffix is None or document["metadata"]["name"].endswith(suffix)) + ] + assert len(matches) == 1, ( + f"expected one {kind} for component {component!r}, got " + f"{[item['metadata']['name'] for item in matches]}" + ) + return matches[0] + + +def container(workload): + return workload["spec"]["template"]["spec"]["containers"][0] + + +def container_mounts(workload): + return { + mount["name"]: (mount["mountPath"], mount.get("subPath")) + for mount in container(workload).get("volumeMounts", []) + } + + +def env_value(workload_container, name): + for entry in workload_container.get("env", []): + if entry["name"] == name: + return entry.get("value") + return None + + +def env_config(workload, documents): + reference = container(workload)["envFrom"][0]["configMapRef"]["name"] + matches = [ + document for document in documents + if document.get("kind") == "ConfigMap" + and document["metadata"]["name"] == reference + ] + assert len(matches) == 1, f"missing env ConfigMap {reference}" + return matches[0]["data"] + + +def check_base(): + documents = render() + server = one(documents, "Deployment", "server") + assert container(server)["args"] == ["start"] + assert env_value(container(server), "WORKER_ENABLED") is None + assert env_value(container(server), "MIGRATION_ENABLED") is None + assert server["metadata"]["labels"]["app.kubernetes.io/version"] == "1.0.0-beta.64" + + configured_values = { + "server": { + "configuration": "core:\n trustProxy: '1'\n", + "provisioning": { + "enabled": True, + "files": {"realms.yaml": "[]\n"}, + }, + } + } + configured = render(configured_values) + configuration = one( + configured, "ConfigMap", "server", suffix="-configuration" + ) + assert set(configuration["data"]) == {"authup.yml"} + server = one(configured, "Deployment", "server") + mounts = container_mounts(server) + assert mounts["configuration"] == ("/etc/authup/authup.yml", "authup.yml") + assert mounts["provisioning"][0] == "/etc/authup/provisioning" + assert mounts["logs"][0] == "/var/log/authup" + environment = env_config(server, configured) + assert environment["PROVISIONING_DIRECTORY_PATH"] == "/etc/authup/provisioning" + assert environment["LOG_DIRECTORY_PATH"] == "/var/log/authup" + assert "WRITABLE_DIRECTORY_PATH" not in environment + + migration_values = { + "server": { + "configuration": "core:\n trustProxy: '1'\n", + "migration": {"enabled": True}, + } + } + migration_documents = render(migration_values) + migration = one(migration_documents, "Job", "migration") + assert container(migration)["args"] == ["migration", "run"] + migration_mounts = container_mounts(migration) + assert migration_mounts["configuration"] == ( + "/etc/authup/authup.yml", + "authup.yml", + ) + assert migration_mounts["logs"][0] == "/var/log/authup" + + +checks = { + "base": check_base, + "all": check_base, +} + +if case not in checks: + raise SystemExit(f"unknown contract case {case!r}: choose {', '.join(checks)}") + +checks[case]() +print(f"beta.64 {case} contract OK") From cb69aef86c5520a82a1a27ab0fe990242086b68b Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:33:11 +0200 Subject: [PATCH 02/14] feat(authup): add split console workloads --- charts/authup/ci/split-values.yaml | 39 + .../authup/templates/_admin-console-env.tpl | 23 - charts/authup/templates/_console-env.tpl | 69 + charts/authup/templates/_helpers.tpl | 8 + charts/authup/templates/_server-env.tpl | 2 + charts/authup/templates/_urls.tpl | 28 +- .../account-console/configmap-env.yaml | 10 + .../templates/account-console/deployment.yaml | 162 ++ .../authup/templates/account-console/hpa.yaml | 33 + .../authup/templates/account-console/pdb.yaml | 20 + .../templates/account-console/service.yaml | 47 + .../admin-console/configmap-env.yaml | 4 +- .../templates/admin-console/deployment.yaml | 29 +- .../authup/templates/admin-console/hpa.yaml | 2 +- .../authup/templates/admin-console/pdb.yaml | 2 +- .../templates/admin-console/service.yaml | 2 +- .../templates/auth-console/configmap-env.yaml | 10 + .../templates/auth-console/deployment.yaml | 162 ++ charts/authup/templates/auth-console/hpa.yaml | 33 + charts/authup/templates/auth-console/pdb.yaml | 20 + .../templates/auth-console/service.yaml | 47 + .../authup/templates/server/deployment.yaml | 3 + charts/authup/templates/validations.yaml | 15 +- charts/authup/values.schema.json | 2395 ++++++++++++++++- charts/authup/values.yaml | 608 ++++- scripts/check-beta64-contract.py | 73 +- 26 files changed, 3594 insertions(+), 252 deletions(-) create mode 100644 charts/authup/ci/split-values.yaml delete mode 100644 charts/authup/templates/_admin-console-env.tpl create mode 100644 charts/authup/templates/_console-env.tpl create mode 100644 charts/authup/templates/account-console/configmap-env.yaml create mode 100644 charts/authup/templates/account-console/deployment.yaml create mode 100644 charts/authup/templates/account-console/hpa.yaml create mode 100644 charts/authup/templates/account-console/pdb.yaml create mode 100644 charts/authup/templates/account-console/service.yaml create mode 100644 charts/authup/templates/auth-console/configmap-env.yaml create mode 100644 charts/authup/templates/auth-console/deployment.yaml create mode 100644 charts/authup/templates/auth-console/hpa.yaml create mode 100644 charts/authup/templates/auth-console/pdb.yaml create mode 100644 charts/authup/templates/auth-console/service.yaml diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml new file mode 100644 index 0000000..535f9e0 --- /dev/null +++ b/charts/authup/ci/split-values.yaml @@ -0,0 +1,39 @@ +# Split API, console and worker topology against the bundled services. +postgresql: + persistence: + enabled: false +valkey: + enabled: true +server: + splitConsoles: true + replicaCount: 2 + migration: + enabled: true + ingress: + enabled: true + hostname: auth.example.com + tls: true + networkPolicy: + enabled: true + allowExternalEgress: false + resources: + requests: + cpu: 50m + memory: 256Mi +authConsole: + resources: + requests: + cpu: 25m + memory: 128Mi +adminConsole: + enabled: true + resources: + requests: + cpu: 25m + memory: 128Mi +accountConsole: + enabled: true + resources: + requests: + cpu: 25m + memory: 128Mi diff --git a/charts/authup/templates/_admin-console-env.tpl b/charts/authup/templates/_admin-console-env.tpl deleted file mode 100644 index c5d101d..0000000 --- a/charts/authup/templates/_admin-console-env.tpl +++ /dev/null @@ -1,23 +0,0 @@ -{{/* -client-admin-console environment as a YAML map. The published UI bundle only honors Nuxt -runtime-config names (NUXT_*); the API URL must be the BROWSER-reachable -server-core URL, never a cluster-internal service name. -The chart deliberately never sets NUXT_PUBLIC_COOKIE_DOMAIN: sharing a cookie -domain between client-admin-console and the hosted auth pages is unsupported by authup. -*/}} -{{- define "authup.adminConsole.configEnv" -}} -{{- $apiUrl := include "authup.adminConsole.apiUrl" . }} -{{- if $apiUrl }} -NUXT_PUBLIC_API_URL: {{ $apiUrl | quote }} -{{- end }} -{{- $publicUrl := include "authup.adminConsole.publicUrl" . }} -{{- if $publicUrl }} -NUXT_PUBLIC_PUBLIC_URL: {{ $publicUrl | quote }} -{{- end }} -{{- if .Values.adminConsole.internalApiUrl }} -NUXT_API_URL: {{ include "authup.tplvalues.render" (dict "value" .Values.adminConsole.internalApiUrl "context" $) | quote }} -{{- end }} -{{- range $key, $value := .Values.adminConsole.config }} -{{ $key }}: {{ include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $) | quote }} -{{- end }} -{{- end -}} diff --git a/charts/authup/templates/_console-env.tpl b/charts/authup/templates/_console-env.tpl new file mode 100644 index 0000000..37afae9 --- /dev/null +++ b/charts/authup/templates/_console-env.tpl @@ -0,0 +1,69 @@ +{{/* Non-secret environment shared by one split console workload. */}} +{{- define "authup.console.configEnv" -}} +{{- $ctx := required "authup.console.configEnv: context is required" .context -}} +{{- $values := required "authup.console.configEnv: values are required" .values -}} +{{- $portName := printf "%s_CONSOLE_PORT" .prefix -}} +{{- $publicUrl := include "authup.server.publicUrl" $ctx -}} +{{- if $publicUrl }} +PUBLIC_URL: {{ $publicUrl | quote }} +{{- end }} +INTERNAL_URL: {{ printf "http://%s:%v" (include "authup.server.fullname" $ctx) $ctx.Values.server.service.ports.http | quote }} +ADMIN_CONSOLE_ENABLED: {{ $ctx.Values.adminConsole.enabled | toString | quote }} +ACCOUNT_CONSOLE_ENABLED: {{ $ctx.Values.accountConsole.enabled | toString | quote }} +{{ $portName }}: {{ $values.containerPorts.http | toString | quote }} +{{- if include "authup.server.themeMounted" $ctx }} +{{ include "authup.server.themeEnv" $ctx }} +{{- end }} +{{- $reserved := list "PUBLIC_URL" "INTERNAL_URL" "ADMIN_CONSOLE_ENABLED" "ACCOUNT_CONSOLE_ENABLED" $portName "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" }} +{{- range $key, $value := $values.config }} +{{- if has $key $reserved }} +{{- fail (printf "authup: %s.config.%s collides with a first-class chart value; set the dedicated value instead." .key $key) }} +{{- end }} +{{ $key }}: {{ include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $ctx) | quote }} +{{- end }} +{{- end -}} + +{{/* Configuration, theme and temporary mounts used by split consoles. */}} +{{- define "authup.console.volumeMounts" -}} +{{- $ctx := required "authup.console.volumeMounts: context is required" .context -}} +- name: tmp + mountPath: /tmp +{{- if or $ctx.Values.server.configuration $ctx.Values.server.existingConfigmap }} +- name: configuration + mountPath: /etc/authup/authup.yml + subPath: authup.yml + readOnly: true +{{- end }} +{{- if include "authup.server.themeMounted" $ctx }} +- name: theme + mountPath: {{ include "authup.server.themeMountPath" $ctx }} + readOnly: true +{{- end }} +{{- end -}} + +{{- define "authup.console.volumes" -}} +{{- $ctx := required "authup.console.volumes: context is required" .context -}} +- name: tmp + emptyDir: {} +{{- if or $ctx.Values.server.configuration $ctx.Values.server.existingConfigmap }} +- name: configuration + configMap: + name: {{ include "authup.server.configurationConfigMapName" (dict "context" $ctx "hook" false) }} +{{- end }} +{{- if include "authup.server.themeMounted" $ctx }} +- name: theme + configMap: + name: {{ include "authup.server.themeConfigMapName" $ctx }} + {{- if $ctx.Values.server.theme.existingConfigMap }} + {{- with $ctx.Values.server.theme.existingConfigMapItems }} + items: {{- include "authup.tplvalues.render" (dict "value" . "context" $ctx) | nindent 6 }} + {{- end }} + {{- else }} + items: + {{- range $path := splitList "\n" (include "authup.server.themePaths" $ctx) }} + - key: {{ include "authup.server.themeConfigMapKey" $path }} + path: {{ $path }} + {{- end }} + {{- end }} +{{- end }} +{{- end -}} diff --git a/charts/authup/templates/_helpers.tpl b/charts/authup/templates/_helpers.tpl index dd774e3..4e088cb 100644 --- a/charts/authup/templates/_helpers.tpl +++ b/charts/authup/templates/_helpers.tpl @@ -57,6 +57,14 @@ Usage: {{ include "authup.component.fullname" (dict "context" $ "suffix" "server {{- include "authup.component.fullname" (dict "context" . "suffix" "admin-console") -}} {{- end -}} +{{- define "authup.authConsole.fullname" -}} +{{- include "authup.component.fullname" (dict "context" . "suffix" "auth-console") -}} +{{- end -}} + +{{- define "authup.accountConsole.fullname" -}} +{{- include "authup.component.fullname" (dict "context" . "suffix" "account-console") -}} +{{- end -}} + {{- define "authup.postgresql.fullname" -}} {{- include "authup.component.fullname" (dict "context" . "suffix" "postgresql") -}} {{- end -}} diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index ac1290c..ebf6a84 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -26,6 +26,8 @@ TRUST_PROXY: {{ .Values.server.trustProxy | toString | quote }} REGISTRATION_ENABLED: {{ .Values.server.features.registration | toString | quote }} PASSWORD_RECOVERY_ENABLED: {{ .Values.server.features.passwordRecovery | toString | quote }} EMAIL_VERIFICATION_ENABLED: {{ .Values.server.features.emailVerification | toString | quote }} +ACCOUNT_CONSOLE_ENABLED: {{ .Values.accountConsole.enabled | toString | quote }} +ADMIN_CONSOLE_ENABLED: {{ .Values.adminConsole.enabled | toString | quote }} MFA_ENABLED: {{ .Values.server.mfa.enabled | toString | quote }} MFA_REQUIRED: {{ .Values.server.mfa.required | toString | quote }} {{- if .Values.auth.adminPasswordReset }} diff --git a/charts/authup/templates/_urls.tpl b/charts/authup/templates/_urls.tpl index 15f0c8e..0de8f3b 100644 --- a/charts/authup/templates/_urls.tpl +++ b/charts/authup/templates/_urls.tpl @@ -1,6 +1,6 @@ {{/* -URL derivation. server.publicUrl / adminConsole.publicUrl always win; otherwise the URL is -derived from the component's ingress (scheme from tls/certManager, host, path). +URL derivation. server.publicUrl wins; otherwise the URL is derived from the +server ingress (scheme from tls/certManager, host, path). Returns "" when nothing can be derived. */}} @@ -33,24 +33,14 @@ scheme-less result can never reach an env var or origin derivation. {{- end -}} {{- end -}} +{{/* Compatibility aliases for templates migrated in later slices. */}} {{- define "authup.adminConsole.publicUrl" -}} -{{- if .Values.adminConsole.publicUrl -}} -{{- include "authup.assertUrlScheme" (dict "key" "adminConsole.publicUrl" "url" (include "authup.tplvalues.render" (dict "value" .Values.adminConsole.publicUrl "context" $) | trimSuffix "/")) -}} -{{- else -}} -{{- include "authup.ingress.derivedUrl" (dict "ingress" .Values.adminConsole.ingress "context" $) -}} -{{- end -}} +{{- include "authup.server.publicUrl" . -}} {{- end -}} -{{/* -Browser-facing server-core URL for the UI (NUXT_PUBLIC_API_URL). -*/}} {{- define "authup.adminConsole.apiUrl" -}} -{{- if .Values.adminConsole.apiUrl -}} -{{- include "authup.assertUrlScheme" (dict "key" "adminConsole.apiUrl" "url" (include "authup.tplvalues.render" (dict "value" .Values.adminConsole.apiUrl "context" $) | trimSuffix "/")) -}} -{{- else -}} {{- include "authup.server.publicUrl" . -}} {{- end -}} -{{- end -}} {{/* Extract the origin (scheme://host[:port]) from a URL. @@ -65,8 +55,7 @@ Extract the origin (scheme://host[:port]) from a URL. {{- end -}} {{/* -TRUSTED_ORIGINS: the user-supplied list/string, plus the UI origin unless -disabled or already covered by the server public URL's origin. +TRUSTED_ORIGINS from the user-supplied list/string. */}} {{- define "authup.server.trustedOrigins" -}} {{- $origins := list -}} @@ -82,13 +71,6 @@ disabled or already covered by the server public URL's origin. {{- $origins = append $origins (trim (include "authup.tplvalues.render" (dict "value" . "context" $))) -}} {{- end -}} {{- end -}} -{{- if and .Values.adminConsole.enabled .Values.server.trustedOriginsAppendAdminConsole -}} -{{- $uiOrigin := include "authup.urlOrigin" (include "authup.adminConsole.publicUrl" .) -}} -{{- $serverOrigin := include "authup.urlOrigin" (include "authup.server.publicUrl" .) -}} -{{- if and $uiOrigin (ne $uiOrigin $serverOrigin) -}} -{{- $origins = append $origins $uiOrigin -}} -{{- end -}} -{{- end -}} {{- $origins = $origins | uniq -}} {{- range $origins -}} {{- include "authup.assertTrustedOrigin" . -}} diff --git a/charts/authup/templates/account-console/configmap-env.yaml b/charts/authup/templates/account-console/configmap-env.yaml new file mode 100644 index 0000000..07c2d24 --- /dev/null +++ b/charts/authup/templates/account-console/configmap-env.yaml @@ -0,0 +1,10 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-env" (include "authup.accountConsole.fullname" .) }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +data: {{- include "authup.console.configEnv" (dict "context" $ "values" .Values.accountConsole "key" "accountConsole" "prefix" "ACCOUNT") | nindent 2 }} +{{- end }} diff --git a/charts/authup/templates/account-console/deployment.yaml b/charts/authup/templates/account-console/deployment.yaml new file mode 100644 index 0000000..55cab81 --- /dev/null +++ b/charts/authup/templates/account-console/deployment.yaml @@ -0,0 +1,162 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if not .Values.accountConsole.autoscaling.hpa.enabled }} + replicas: {{ .Values.accountConsole.replicaCount }} + {{- end }} + revisionHistoryLimit: {{ .Values.accountConsole.revisionHistoryLimit }} + {{- if .Values.accountConsole.updateStrategy }} + strategy: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.updateStrategy "context" $) | nindent 4 }} + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "account-console") | nindent 6 }} + template: + metadata: + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 8 }} + {{- if .Values.accountConsole.podLabels }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.podLabels "context" $) | nindent 8 }} + {{- end }} + annotations: + {{- if not .Values.accountConsole.disableRestartOnChanges }} + checksum/env: {{ include "authup.console.configEnv" (dict "context" $ "values" .Values.accountConsole "key" "accountConsole" "prefix" "ACCOUNT") | sha256sum }} + {{- if .Values.server.configuration }} + checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} + {{- end }} + {{- if and .Values.server.theme.enabled (or .Values.server.theme.files (include "authup.server.themeManifestConfigured" .)) }} + checksum/theme: {{ include (print $.Template.BasePath "/server/configmap-theme.yaml") . | sha256sum }} + {{- end }} + {{- end }} + {{- if .Values.accountConsole.podAnnotations }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.podAnnotations "context" $) | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "authup.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + {{- include "authup.imagePullSecrets" . | nindent 6 }} + {{- if .Values.accountConsole.podSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.accountConsole.podSecurityContext | nindent 8 }} + {{- end }} + {{- if .Values.accountConsole.priorityClassName }} + priorityClassName: {{ .Values.accountConsole.priorityClassName }} + {{- end }} + {{- if .Values.accountConsole.schedulerName }} + schedulerName: {{ .Values.accountConsole.schedulerName }} + {{- end }} + {{- if .Values.accountConsole.hostAliases }} + hostAliases: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.hostAliases "context" $) | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.accountConsole.terminationGracePeriodSeconds }} + {{- if .Values.accountConsole.affinity }} + affinity: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.affinity "context" $) | nindent 8 }} + {{- else if .Values.accountConsole.podAntiAffinityPreset }} + affinity: {{- include "authup.podAntiAffinity" (dict "context" $ "component" "account-console" "preset" .Values.accountConsole.podAntiAffinityPreset) | nindent 8 }} + {{- end }} + {{- if .Values.accountConsole.nodeSelector }} + nodeSelector: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.accountConsole.tolerations }} + tolerations: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.tolerations "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.accountConsole.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "authup.topologySpreadConstraints" (dict "context" $ "component" "account-console" "constraints" .Values.accountConsole.topologySpreadConstraints) | nindent 8 }} + {{- end }} + {{- if .Values.accountConsole.initContainers }} + initContainers: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: account-console + image: {{ include "authup.appImage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.accountConsole.containerSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.accountConsole.containerSecurityContext | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- toYaml .Values.diagnosticMode.command | nindent 12 }} + args: {{- toYaml .Values.diagnosticMode.args | nindent 12 }} + {{- else }} + {{- if .Values.accountConsole.command }} + command: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.accountConsole.args }} + args: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.args "context" $) | nindent 12 }} + {{- else }} + args: + - start + - console + - account + {{- end }} + {{- end }} + envFrom: + - configMapRef: + name: {{ printf "%s-env" (include "authup.accountConsole.fullname" .) }} + {{- if .Values.accountConsole.extraEnvVarsCM }} + - configMapRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.accountConsole.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.accountConsole.extraEnvVarsSecret }} + - secretRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.accountConsole.extraEnvVarsSecret "context" $) }} + {{- end }} + env: + - name: npm_config_cache + value: /tmp/.npm-cache + {{- if .Values.accountConsole.extraEnvVars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.accountConsole.containerPorts.http }} + protocol: TCP + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.accountConsole.customStartupProbe }} + startupProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.customStartupProbe "context" $) | nindent 12 }} + {{- else if .Values.accountConsole.startupProbe.enabled }} + startupProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.accountConsole.startupProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.accountConsole.customLivenessProbe }} + livenessProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.customLivenessProbe "context" $) | nindent 12 }} + {{- else if .Values.accountConsole.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.accountConsole.livenessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.accountConsole.customReadinessProbe }} + readinessProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.accountConsole.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.accountConsole.readinessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- end }} + {{- if .Values.accountConsole.resources }} + resources: {{- toYaml .Values.accountConsole.resources | nindent 12 }} + {{- end }} + {{- if .Values.accountConsole.lifecycleHooks }} + lifecycle: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: {{- include "authup.console.volumeMounts" (dict "context" $) | nindent 12 }} + {{- if .Values.accountConsole.extraVolumeMounts }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.accountConsole.sidecars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: {{- include "authup.console.volumes" (dict "context" $) | nindent 8 }} + {{- if .Values.accountConsole.extraVolumes }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.extraVolumes "context" $) | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/account-console/hpa.yaml b/charts/authup/templates/account-console/hpa.yaml new file mode 100644 index 0000000..52da794 --- /dev/null +++ b/charts/authup/templates/account-console/hpa.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.autoscaling.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "authup.accountConsole.fullname" . }} + minReplicas: {{ .Values.accountConsole.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.accountConsole.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.accountConsole.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.accountConsole.autoscaling.hpa.targetCPU }} + {{- end }} + {{- if .Values.accountConsole.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.accountConsole.autoscaling.hpa.targetMemory }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/account-console/pdb.yaml b/charts/authup/templates/account-console/pdb.yaml new file mode 100644 index 0000000..64e6fdf --- /dev/null +++ b/charts/authup/templates/account-console/pdb.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.pdb.create }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if .Values.accountConsole.pdb.minAvailable }} + minAvailable: {{ .Values.accountConsole.pdb.minAvailable }} + {{- end }} + {{- if .Values.accountConsole.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.accountConsole.pdb.maxUnavailable }} + {{- else if not .Values.accountConsole.pdb.minAvailable }} + maxUnavailable: 1 + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "account-console") | nindent 6 }} +{{- end }} diff --git a/charts/authup/templates/account-console/service.yaml b/charts/authup/templates/account-console/service.yaml new file mode 100644 index 0000000..952d383 --- /dev/null +++ b/charts/authup/templates/account-console/service.yaml @@ -0,0 +1,47 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if .Values.accountConsole.service.annotations }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.service.annotations "context" $) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.accountConsole.service.type }} + {{- if and .Values.accountConsole.service.clusterIP (eq .Values.accountConsole.service.type "ClusterIP") }} + clusterIP: {{ .Values.accountConsole.service.clusterIP }} + {{- end }} + {{- if eq .Values.accountConsole.service.type "LoadBalancer" }} + {{- if .Values.accountConsole.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.accountConsole.service.loadBalancerIP }} + {{- end }} + {{- if .Values.accountConsole.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{- toYaml .Values.accountConsole.service.loadBalancerSourceRanges | nindent 4 }} + {{- end }} + {{- end }} + {{- if or (eq .Values.accountConsole.service.type "LoadBalancer") (eq .Values.accountConsole.service.type "NodePort") }} + externalTrafficPolicy: {{ .Values.accountConsole.service.externalTrafficPolicy | quote }} + {{- end }} + {{- if .Values.accountConsole.service.sessionAffinity }} + sessionAffinity: {{ .Values.accountConsole.service.sessionAffinity }} + {{- end }} + {{- if .Values.accountConsole.service.sessionAffinityConfig }} + sessionAffinityConfig: {{- toYaml .Values.accountConsole.service.sessionAffinityConfig | nindent 4 }} + {{- end }} + ports: + - name: http + port: {{ .Values.accountConsole.service.ports.http }} + targetPort: http + protocol: TCP + {{- if and (or (eq .Values.accountConsole.service.type "NodePort") (eq .Values.accountConsole.service.type "LoadBalancer")) .Values.accountConsole.service.nodePorts.http }} + nodePort: {{ .Values.accountConsole.service.nodePorts.http }} + {{- end }} + {{- if .Values.accountConsole.service.extraPorts }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.service.extraPorts "context" $) | nindent 4 }} + {{- end }} + selector: {{- include "authup.matchLabels" (dict "context" $ "component" "account-console") | nindent 4 }} +{{- end }} diff --git a/charts/authup/templates/admin-console/configmap-env.yaml b/charts/authup/templates/admin-console/configmap-env.yaml index 1799903..e4a3ae1 100644 --- a/charts/authup/templates/admin-console/configmap-env.yaml +++ b/charts/authup/templates/admin-console/configmap-env.yaml @@ -1,4 +1,4 @@ -{{- if .Values.adminConsole.enabled }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled }} apiVersion: v1 kind: ConfigMap metadata: @@ -6,5 +6,5 @@ metadata: namespace: {{ include "authup.namespace" . | quote }} labels: {{- include "authup.labels" (dict "context" $ "component" "admin-console") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} -data: {{- include "authup.adminConsole.configEnv" . | nindent 2 }} +data: {{- include "authup.console.configEnv" (dict "context" $ "values" .Values.adminConsole "key" "adminConsole" "prefix" "ADMIN") | nindent 2 }} {{- end }} diff --git a/charts/authup/templates/admin-console/deployment.yaml b/charts/authup/templates/admin-console/deployment.yaml index 5b3e710..cfd3872 100644 --- a/charts/authup/templates/admin-console/deployment.yaml +++ b/charts/authup/templates/admin-console/deployment.yaml @@ -1,4 +1,4 @@ -{{- if .Values.adminConsole.enabled }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled }} apiVersion: apps/v1 kind: Deployment metadata: @@ -24,7 +24,13 @@ spec: {{- end }} annotations: {{- if not .Values.adminConsole.disableRestartOnChanges }} - checksum/env: {{ include "authup.adminConsole.configEnv" . | sha256sum }} + checksum/env: {{ include "authup.console.configEnv" (dict "context" $ "values" .Values.adminConsole "key" "adminConsole" "prefix" "ADMIN") | sha256sum }} + {{- if .Values.server.configuration }} + checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} + {{- end }} + {{- if and .Values.server.theme.enabled (or .Values.server.theme.files (include "authup.server.themeManifestConfigured" .)) }} + checksum/theme: {{ include (print $.Template.BasePath "/server/configmap-theme.yaml") . | sha256sum }} + {{- end }} {{- end }} {{- if .Values.adminConsole.podAnnotations }} {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.podAnnotations "context" $) | nindent 8 }} @@ -81,8 +87,9 @@ spec: args: {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.args "context" $) | nindent 12 }} {{- else }} args: - - client/admin-console - start + - console + - admin {{- end }} {{- end }} envFrom: @@ -104,7 +111,7 @@ spec: {{- end }} ports: - name: http - containerPort: 3000 + containerPort: {{ .Values.adminConsole.containerPorts.http }} protocol: TCP {{- if not .Values.diagnosticMode.enabled }} {{- if .Values.adminConsole.customStartupProbe }} @@ -112,7 +119,7 @@ spec: {{- else if .Values.adminConsole.startupProbe.enabled }} startupProbe: httpGet: - path: / + path: /healthy port: http {{- omit .Values.adminConsole.startupProbe "enabled" | toYaml | nindent 12 }} {{- end }} @@ -121,7 +128,7 @@ spec: {{- else if .Values.adminConsole.livenessProbe.enabled }} livenessProbe: httpGet: - path: / + path: /healthy port: http {{- omit .Values.adminConsole.livenessProbe "enabled" | toYaml | nindent 12 }} {{- end }} @@ -130,7 +137,7 @@ spec: {{- else if .Values.adminConsole.readinessProbe.enabled }} readinessProbe: httpGet: - path: / + path: /healthy port: http {{- omit .Values.adminConsole.readinessProbe "enabled" | toYaml | nindent 12 }} {{- end }} @@ -141,18 +148,14 @@ spec: {{- if .Values.adminConsole.lifecycleHooks }} lifecycle: {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.lifecycleHooks "context" $) | nindent 12 }} {{- end }} - volumeMounts: - - name: tmp - mountPath: /tmp + volumeMounts: {{- include "authup.console.volumeMounts" (dict "context" $) | nindent 12 }} {{- if .Values.adminConsole.extraVolumeMounts }} {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.extraVolumeMounts "context" $) | nindent 12 }} {{- end }} {{- if .Values.adminConsole.sidecars }} {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.sidecars "context" $) | nindent 8 }} {{- end }} - volumes: - - name: tmp - emptyDir: {} + volumes: {{- include "authup.console.volumes" (dict "context" $) | nindent 8 }} {{- if .Values.adminConsole.extraVolumes }} {{- include "authup.tplvalues.render" (dict "value" .Values.adminConsole.extraVolumes "context" $) | nindent 8 }} {{- end }} diff --git a/charts/authup/templates/admin-console/hpa.yaml b/charts/authup/templates/admin-console/hpa.yaml index f8d1322..a788745 100644 --- a/charts/authup/templates/admin-console/hpa.yaml +++ b/charts/authup/templates/admin-console/hpa.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.adminConsole.enabled .Values.adminConsole.autoscaling.hpa.enabled }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.autoscaling.hpa.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: diff --git a/charts/authup/templates/admin-console/pdb.yaml b/charts/authup/templates/admin-console/pdb.yaml index aa48ff6..6d61ed4 100644 --- a/charts/authup/templates/admin-console/pdb.yaml +++ b/charts/authup/templates/admin-console/pdb.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.adminConsole.enabled .Values.adminConsole.pdb.create }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.pdb.create }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/charts/authup/templates/admin-console/service.yaml b/charts/authup/templates/admin-console/service.yaml index 88d176f..33c335e 100644 --- a/charts/authup/templates/admin-console/service.yaml +++ b/charts/authup/templates/admin-console/service.yaml @@ -1,4 +1,4 @@ -{{- if .Values.adminConsole.enabled }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled }} apiVersion: v1 kind: Service metadata: diff --git a/charts/authup/templates/auth-console/configmap-env.yaml b/charts/authup/templates/auth-console/configmap-env.yaml new file mode 100644 index 0000000..ee52fb2 --- /dev/null +++ b/charts/authup/templates/auth-console/configmap-env.yaml @@ -0,0 +1,10 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-env" (include "authup.authConsole.fullname" .) }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +data: {{- include "authup.console.configEnv" (dict "context" $ "values" .Values.authConsole "key" "authConsole" "prefix" "AUTH") | nindent 2 }} +{{- end }} diff --git a/charts/authup/templates/auth-console/deployment.yaml b/charts/authup/templates/auth-console/deployment.yaml new file mode 100644 index 0000000..d3f67bc --- /dev/null +++ b/charts/authup/templates/auth-console/deployment.yaml @@ -0,0 +1,162 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if not .Values.authConsole.autoscaling.hpa.enabled }} + replicas: {{ .Values.authConsole.replicaCount }} + {{- end }} + revisionHistoryLimit: {{ .Values.authConsole.revisionHistoryLimit }} + {{- if .Values.authConsole.updateStrategy }} + strategy: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.updateStrategy "context" $) | nindent 4 }} + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "auth-console") | nindent 6 }} + template: + metadata: + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 8 }} + {{- if .Values.authConsole.podLabels }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.podLabels "context" $) | nindent 8 }} + {{- end }} + annotations: + {{- if not .Values.authConsole.disableRestartOnChanges }} + checksum/env: {{ include "authup.console.configEnv" (dict "context" $ "values" .Values.authConsole "key" "authConsole" "prefix" "AUTH") | sha256sum }} + {{- if .Values.server.configuration }} + checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} + {{- end }} + {{- if and .Values.server.theme.enabled (or .Values.server.theme.files (include "authup.server.themeManifestConfigured" .)) }} + checksum/theme: {{ include (print $.Template.BasePath "/server/configmap-theme.yaml") . | sha256sum }} + {{- end }} + {{- end }} + {{- if .Values.authConsole.podAnnotations }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.podAnnotations "context" $) | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "authup.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + {{- include "authup.imagePullSecrets" . | nindent 6 }} + {{- if .Values.authConsole.podSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.authConsole.podSecurityContext | nindent 8 }} + {{- end }} + {{- if .Values.authConsole.priorityClassName }} + priorityClassName: {{ .Values.authConsole.priorityClassName }} + {{- end }} + {{- if .Values.authConsole.schedulerName }} + schedulerName: {{ .Values.authConsole.schedulerName }} + {{- end }} + {{- if .Values.authConsole.hostAliases }} + hostAliases: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.hostAliases "context" $) | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.authConsole.terminationGracePeriodSeconds }} + {{- if .Values.authConsole.affinity }} + affinity: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.affinity "context" $) | nindent 8 }} + {{- else if .Values.authConsole.podAntiAffinityPreset }} + affinity: {{- include "authup.podAntiAffinity" (dict "context" $ "component" "auth-console" "preset" .Values.authConsole.podAntiAffinityPreset) | nindent 8 }} + {{- end }} + {{- if .Values.authConsole.nodeSelector }} + nodeSelector: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.authConsole.tolerations }} + tolerations: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.tolerations "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.authConsole.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "authup.topologySpreadConstraints" (dict "context" $ "component" "auth-console" "constraints" .Values.authConsole.topologySpreadConstraints) | nindent 8 }} + {{- end }} + {{- if .Values.authConsole.initContainers }} + initContainers: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: auth-console + image: {{ include "authup.appImage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.authConsole.containerSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.authConsole.containerSecurityContext | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- toYaml .Values.diagnosticMode.command | nindent 12 }} + args: {{- toYaml .Values.diagnosticMode.args | nindent 12 }} + {{- else }} + {{- if .Values.authConsole.command }} + command: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.authConsole.args }} + args: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.args "context" $) | nindent 12 }} + {{- else }} + args: + - start + - console + - auth + {{- end }} + {{- end }} + envFrom: + - configMapRef: + name: {{ printf "%s-env" (include "authup.authConsole.fullname" .) }} + {{- if .Values.authConsole.extraEnvVarsCM }} + - configMapRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.authConsole.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.authConsole.extraEnvVarsSecret }} + - secretRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.authConsole.extraEnvVarsSecret "context" $) }} + {{- end }} + env: + - name: npm_config_cache + value: /tmp/.npm-cache + {{- if .Values.authConsole.extraEnvVars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.authConsole.containerPorts.http }} + protocol: TCP + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.authConsole.customStartupProbe }} + startupProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.customStartupProbe "context" $) | nindent 12 }} + {{- else if .Values.authConsole.startupProbe.enabled }} + startupProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.authConsole.startupProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.authConsole.customLivenessProbe }} + livenessProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.customLivenessProbe "context" $) | nindent 12 }} + {{- else if .Values.authConsole.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.authConsole.livenessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.authConsole.customReadinessProbe }} + readinessProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.authConsole.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: /healthy + port: http + {{- omit .Values.authConsole.readinessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- end }} + {{- if .Values.authConsole.resources }} + resources: {{- toYaml .Values.authConsole.resources | nindent 12 }} + {{- end }} + {{- if .Values.authConsole.lifecycleHooks }} + lifecycle: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: {{- include "authup.console.volumeMounts" (dict "context" $) | nindent 12 }} + {{- if .Values.authConsole.extraVolumeMounts }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.authConsole.sidecars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: {{- include "authup.console.volumes" (dict "context" $) | nindent 8 }} + {{- if .Values.authConsole.extraVolumes }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.extraVolumes "context" $) | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/auth-console/hpa.yaml b/charts/authup/templates/auth-console/hpa.yaml new file mode 100644 index 0000000..d904104 --- /dev/null +++ b/charts/authup/templates/auth-console/hpa.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.autoscaling.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "authup.authConsole.fullname" . }} + minReplicas: {{ .Values.authConsole.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.authConsole.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.authConsole.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.authConsole.autoscaling.hpa.targetCPU }} + {{- end }} + {{- if .Values.authConsole.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.authConsole.autoscaling.hpa.targetMemory }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/auth-console/pdb.yaml b/charts/authup/templates/auth-console/pdb.yaml new file mode 100644 index 0000000..ebaa9c9 --- /dev/null +++ b/charts/authup/templates/auth-console/pdb.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.pdb.create }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if .Values.authConsole.pdb.minAvailable }} + minAvailable: {{ .Values.authConsole.pdb.minAvailable }} + {{- end }} + {{- if .Values.authConsole.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.authConsole.pdb.maxUnavailable }} + {{- else if not .Values.authConsole.pdb.minAvailable }} + maxUnavailable: 1 + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "auth-console") | nindent 6 }} +{{- end }} diff --git a/charts/authup/templates/auth-console/service.yaml b/charts/authup/templates/auth-console/service.yaml new file mode 100644 index 0000000..56e72a8 --- /dev/null +++ b/charts/authup/templates/auth-console/service.yaml @@ -0,0 +1,47 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if .Values.authConsole.service.annotations }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.service.annotations "context" $) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.authConsole.service.type }} + {{- if and .Values.authConsole.service.clusterIP (eq .Values.authConsole.service.type "ClusterIP") }} + clusterIP: {{ .Values.authConsole.service.clusterIP }} + {{- end }} + {{- if eq .Values.authConsole.service.type "LoadBalancer" }} + {{- if .Values.authConsole.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.authConsole.service.loadBalancerIP }} + {{- end }} + {{- if .Values.authConsole.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{- toYaml .Values.authConsole.service.loadBalancerSourceRanges | nindent 4 }} + {{- end }} + {{- end }} + {{- if or (eq .Values.authConsole.service.type "LoadBalancer") (eq .Values.authConsole.service.type "NodePort") }} + externalTrafficPolicy: {{ .Values.authConsole.service.externalTrafficPolicy | quote }} + {{- end }} + {{- if .Values.authConsole.service.sessionAffinity }} + sessionAffinity: {{ .Values.authConsole.service.sessionAffinity }} + {{- end }} + {{- if .Values.authConsole.service.sessionAffinityConfig }} + sessionAffinityConfig: {{- toYaml .Values.authConsole.service.sessionAffinityConfig | nindent 4 }} + {{- end }} + ports: + - name: http + port: {{ .Values.authConsole.service.ports.http }} + targetPort: http + protocol: TCP + {{- if and (or (eq .Values.authConsole.service.type "NodePort") (eq .Values.authConsole.service.type "LoadBalancer")) .Values.authConsole.service.nodePorts.http }} + nodePort: {{ .Values.authConsole.service.nodePorts.http }} + {{- end }} + {{- if .Values.authConsole.service.extraPorts }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.service.extraPorts "context" $) | nindent 4 }} + {{- end }} + selector: {{- include "authup.matchLabels" (dict "context" $ "component" "auth-console") | nindent 4 }} +{{- end }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index b90560e..f42a479 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -107,6 +107,9 @@ spec: {{- else }} args: - start + {{- if .Values.server.splitConsoles }} + - core + {{- end }} {{- end }} {{- end }} envFrom: diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index 65cf34d..01484b5 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -45,10 +45,6 @@ database is configured while the mounted file is the operator's. {{- fail "authup: server.configuration and server.existingConfigmap are mutually exclusive. The existing ConfigMap is the one that gets mounted, so the inline content would be silently dropped: move it into that ConfigMap, or drop server.existingConfigmap." }} {{- end }} -{{- if and .Values.adminConsole.enabled (not .Values.server.enabled) (not .Values.adminConsole.apiUrl) }} -{{- fail "authup: adminConsole.enabled without server.enabled requires adminConsole.apiUrl (the browser-reachable URL of an external authup server-core)." }} -{{- end }} - {{/* An external database password must be deliberate — never silently generated for a database the chart does not own. */}} {{- if and (not .Values.postgresql.enabled) (not .Values.mysql.enabled) .Values.externalDatabase.host (not .Values.externalDatabase.password) (not .Values.externalDatabase.existingSecret) }} {{- fail "authup: externalDatabase.host is set but neither externalDatabase.password nor externalDatabase.existingSecret is — the chart will not invent a password for a database it does not manage." }} @@ -84,15 +80,8 @@ serves at "/" and expects the proxy to strip the prefix, hence match AND rewrite {{- 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 (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) }} -{{- end }} -{{- end }} - -{{/* URL values must carry a scheme — a scheme-less value would derive a broken "://" origin into TRUSTED_ORIGINS and crash-loop server-core. */}} -{{- range $key, $value := dict "server.publicUrl" .Values.server.publicUrl "adminConsole.publicUrl" .Values.adminConsole.publicUrl "adminConsole.apiUrl" .Values.adminConsole.apiUrl "adminConsole.internalApiUrl" .Values.adminConsole.internalApiUrl }} +{{/* URL values must carry a scheme. */}} +{{- range $key, $value := dict "server.publicUrl" .Values.server.publicUrl }} {{- if and $value (not (contains "{{" $value)) (not (regexMatch "^https?://" $value)) }} {{- fail (printf "authup: %s must be a full URL including the http(s):// scheme (got %q)." $key $value) }} {{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 51aaf40..f886aca 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -2,7 +2,2209 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { + "accountConsole": { + "additionalProperties": false, + "properties": { + "affinity": { + "additionalProperties": true, + "description": "Affinity (overrides the anti-affinity preset when set)", + "required": [], + "title": "affinity" + }, + "args": { + "description": "Override the container args", + "items": { + "required": [] + }, + "required": [], + "title": "args", + "type": "array" + }, + "autoscaling": { + "additionalProperties": false, + "properties": { + "hpa": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": false, + "description": "Enable HPA for the UI", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "maxReplicas": { + "default": 5, + "description": "Maximum replicas", + "required": [], + "title": "maxReplicas", + "type": "integer" + }, + "minReplicas": { + "default": 2, + "description": "Minimum replicas", + "required": [], + "title": "minReplicas", + "type": "integer" + }, + "targetCPU": { + "default": 75, + "description": "Target CPU utilization percentage", + "required": [], + "title": "targetCPU", + "type": "integer" + }, + "targetMemory": { + "default": "", + "description": "Target memory utilization percentage", + "required": [], + "title": "targetMemory", + "type": "string" + } + }, + "required": [ + "enabled", + "minReplicas", + "maxReplicas", + "targetCPU", + "targetMemory" + ], + "title": "hpa", + "type": "object" + } + }, + "required": [ + "hpa" + ], + "title": "autoscaling", + "type": "object" + }, + "command": { + "description": "Override the container command", + "items": { + "required": [] + }, + "required": [], + "title": "command", + "type": "array" + }, + "config": { + "additionalProperties": true, + "description": "Extra environment variables rendered literally into the env ConfigMap", + "required": [], + "title": "config" + }, + "containerPorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3022, + "description": "Account console listener port", + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "containerPorts", + "type": "object" + }, + "containerSecurityContext": { + "additionalProperties": true, + "description": "Container security context (same root-image caveat as the server)", + "properties": { + "allowPrivilegeEscalation": { + "default": false, + "required": [], + "title": "allowPrivilegeEscalation", + "type": "boolean" + }, + "capabilities": { + "additionalProperties": false, + "properties": { + "drop": { + "items": { + "anyOf": [ + { + "required": [], + "type": "string" + } + ], + "required": [] + }, + "required": [], + "title": "drop", + "type": "array" + } + }, + "required": [ + "drop" + ], + "title": "capabilities", + "type": "object" + }, + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "default": false, + "required": [], + "title": "readOnlyRootFilesystem", + "type": "boolean" + }, + "runAsNonRoot": { + "default": false, + "required": [], + "title": "runAsNonRoot", + "type": "boolean" + }, + "runAsUser": { + "default": 0, + "required": [], + "title": "runAsUser", + "type": "integer" + }, + "seccompProfile": { + "additionalProperties": false, + "properties": { + "type": { + "default": "RuntimeDefault", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "seccompProfile", + "type": "object" + } + }, + "required": [ + "enabled", + "runAsUser", + "runAsNonRoot", + "allowPrivilegeEscalation", + "capabilities", + "seccompProfile", + "readOnlyRootFilesystem" + ], + "title": "containerSecurityContext" + }, + "customLivenessProbe": { + "additionalProperties": true, + "description": "Custom liveness probe", + "required": [], + "title": "customLivenessProbe" + }, + "customReadinessProbe": { + "additionalProperties": true, + "description": "Custom readiness probe", + "required": [], + "title": "customReadinessProbe" + }, + "customStartupProbe": { + "additionalProperties": true, + "description": "Custom startup probe", + "required": [], + "title": "customStartupProbe" + }, + "disableRestartOnChanges": { + "default": false, + "description": "Disable the checksum annotations that roll pods on config changes", + "required": [], + "title": "disableRestartOnChanges", + "type": "boolean" + }, + "enabled": { + "default": true, + "description": "Enable the account console and deploy it separately in split-console mode", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEnvVars": { + "description": "Extra environment variables for the account console container", + "items": { + "required": [] + }, + "required": [], + "title": "extraEnvVars", + "type": "array" + }, + "extraEnvVarsCM": { + "default": "", + "description": "Extra ConfigMap with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsCM", + "type": "string" + }, + "extraEnvVarsSecret": { + "default": "", + "description": "Extra Secret with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsSecret", + "type": "string" + }, + "extraVolumeMounts": { + "description": "Extra volume mounts (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumeMounts", + "type": "array" + }, + "extraVolumes": { + "description": "Extra volumes (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumes", + "type": "array" + }, + "hostAliases": { + "description": "Pod host aliases", + "items": { + "required": [] + }, + "required": [], + "title": "hostAliases", + "type": "array" + }, + "ingress": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "Ingress annotations (tpl-rendered)", + "required": [], + "title": "annotations" + }, + "certManager": { + "default": false, + "description": "Request a cert-manager certificate (adds kubernetes.io/tls-acme)", + "required": [], + "title": "certManager", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Enable ingress-nginx routing for the split account console", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraHosts": { + "description": "Extra hosts", + "items": { + "required": [] + }, + "required": [], + "title": "extraHosts", + "type": "array" + }, + "extraPaths": { + "description": "Extra paths for the primary host", + "items": { + "required": [] + }, + "required": [], + "title": "extraPaths", + "type": "array" + }, + "extraRules": { + "description": "Full custom rules (tpl-rendered; appended after the generated rules)", + "items": { + "required": [] + }, + "required": [], + "title": "extraRules", + "type": "array" + }, + "extraTls": { + "description": "Extra TLS entries", + "items": { + "required": [] + }, + "required": [], + "title": "extraTls", + "type": "array" + }, + "hostname": { + "default": "", + "description": "Ingress hostname (tpl-rendered); also drives the derived UI public URL", + "required": [], + "title": "hostname", + "type": "string" + }, + "ingressClassName": { + "default": "", + "description": "Ingress class name", + "required": [], + "title": "ingressClassName", + "type": "string" + }, + "path": { + "default": "/console/account", + "description": "Public console path (the generated ingress strips it)", + "required": [], + "title": "path", + "type": "string" + }, + "pathType": { + "default": "Prefix", + "description": "Ingress path type", + "required": [], + "title": "pathType", + "type": "string" + }, + "tls": { + "default": false, + "description": "Enable TLS for the hostname", + "required": [], + "title": "tls", + "type": "boolean" + } + }, + "required": [ + "enabled", + "hostname", + "path", + "pathType", + "ingressClassName", + "tls", + "certManager", + "extraHosts", + "extraPaths", + "extraTls", + "extraRules" + ], + "title": "ingress", + "type": "object" + }, + "initContainers": { + "description": "Init containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "initContainers", + "type": "array" + }, + "lifecycleHooks": { + "additionalProperties": true, + "description": "Container lifecycle hooks", + "required": [], + "title": "lifecycleHooks" + }, + "livenessProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the liveness probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 3, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 0, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 30, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "livenessProbe", + "type": "object" + }, + "networkPolicy": { + "additionalProperties": false, + "properties": { + "allowExternal": { + "default": true, + "description": "Allow ingress from anywhere", + "required": [], + "title": "allowExternal", + "type": "boolean" + }, + "allowExternalEgress": { + "default": true, + "description": "Allow all egress", + "required": [], + "title": "allowExternalEgress", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Create a NetworkPolicy for the UI", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEgress": { + "description": "Extra egress rules", + "items": { + "required": [] + }, + "required": [], + "title": "extraEgress", + "type": "array" + }, + "extraIngress": { + "description": "Extra ingress rules", + "items": { + "required": [] + }, + "required": [], + "title": "extraIngress", + "type": "array" + }, + "ingressNSMatchLabels": { + "additionalProperties": true, + "description": "Namespace labels allowed to connect when allowExternal is false", + "required": [], + "title": "ingressNSMatchLabels" + }, + "ingressPodMatchLabels": { + "additionalProperties": true, + "description": "Pod labels allowed to connect when allowExternal is false", + "required": [], + "title": "ingressPodMatchLabels" + } + }, + "required": [ + "enabled", + "allowExternal", + "allowExternalEgress", + "extraIngress", + "extraEgress" + ], + "title": "networkPolicy", + "type": "object" + }, + "nodeSelector": { + "additionalProperties": true, + "description": "Node selector", + "required": [], + "title": "nodeSelector" + }, + "pdb": { + "additionalProperties": false, + "properties": { + "create": { + "default": false, + "description": "Create a PodDisruptionBudget for the UI", + "required": [], + "title": "create", + "type": "boolean" + }, + "maxUnavailable": { + "default": "", + "description": "Maximum unavailable pods (defaults to 1 when both are empty)", + "required": [], + "title": "maxUnavailable", + "type": "string" + }, + "minAvailable": { + "default": "", + "description": "Minimum available pods", + "required": [], + "title": "minAvailable", + "type": "string" + } + }, + "required": [ + "create", + "minAvailable", + "maxUnavailable" + ], + "title": "pdb", + "type": "object" + }, + "podAnnotations": { + "additionalProperties": true, + "description": "Pod annotations (tpl-rendered)", + "required": [], + "title": "podAnnotations" + }, + "podAntiAffinityPreset": { + "default": "soft", + "description": "Pod anti-affinity preset: soft, hard or \"\"", + "required": [], + "title": "podAntiAffinityPreset", + "type": "string" + }, + "podLabels": { + "additionalProperties": true, + "description": "Pod labels (tpl-rendered)", + "required": [], + "title": "podLabels" + }, + "podSecurityContext": { + "additionalProperties": true, + "description": "Pod security context", + "properties": { + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "fsGroup": { + "default": 1000, + "required": [], + "title": "fsGroup", + "type": "integer" + } + }, + "required": [ + "enabled", + "fsGroup" + ], + "title": "podSecurityContext" + }, + "priorityClassName": { + "default": "", + "description": "Priority class name", + "required": [], + "title": "priorityClassName", + "type": "string" + }, + "readinessProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the readiness probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 3, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 0, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 10, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "readinessProbe", + "type": "object" + }, + "replicaCount": { + "default": 1, + "description": "Number of account console replicas", + "required": [], + "title": "replicaCount", + "type": "integer" + }, + "resources": { + "additionalProperties": true, + "description": "Account console container resources", + "properties": { + "limits": { + "additionalProperties": true, + "properties": { + "memory": { + "default": "512Mi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "memory" + ], + "title": "limits" + }, + "requests": { + "additionalProperties": true, + "properties": { + "cpu": { + "default": "100m", + "required": [], + "title": "cpu", + "type": "string" + }, + "memory": { + "default": "256Mi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "cpu", + "memory" + ], + "title": "requests" + } + }, + "required": [], + "title": "resources" + }, + "revisionHistoryLimit": { + "default": 3, + "description": "Deployment revision history limit", + "required": [], + "title": "revisionHistoryLimit", + "type": "integer" + }, + "route": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "HTTPRoute annotations", + "required": [], + "title": "annotations" + }, + "enabled": { + "default": false, + "description": "Create a Gateway API HTTPRoute for the account console (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": [ + "boolean", + "string" + ] + }, + "filters": { + "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", + "items": { + "required": [] + }, + "required": [], + "title": "filters", + "type": "array" + }, + "hostnames": { + "description": "Route hostnames ([] = derived from server.publicUrl)", + "items": { + "required": [] + }, + "required": [], + "title": "hostnames", + "type": "array" + }, + "matches": { + "description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"", + "items": { + "required": [] + }, + "required": [], + "title": "matches", + "type": "array" + }, + "parentRefs": { + "description": "Gateway parentRefs", + "items": { + "required": [] + }, + "required": [], + "title": "parentRefs", + "type": "array" + } + }, + "required": [ + "enabled", + "hostnames", + "parentRefs", + "matches", + "filters" + ], + "title": "route", + "type": "object" + }, + "schedulerName": { + "default": "", + "description": "Scheduler name", + "required": [], + "title": "schedulerName", + "type": "string" + }, + "service": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "Service annotations (tpl-rendered)", + "required": [], + "title": "annotations" + }, + "clusterIP": { + "default": "", + "description": "Static cluster IP", + "required": [], + "title": "clusterIP", + "type": "string" + }, + "externalTrafficPolicy": { + "default": "Cluster", + "description": "External traffic policy", + "required": [], + "title": "externalTrafficPolicy", + "type": "string" + }, + "extraPorts": { + "description": "Extra service ports", + "items": { + "required": [] + }, + "required": [], + "title": "extraPorts", + "type": "array" + }, + "loadBalancerIP": { + "default": "", + "description": "LoadBalancer IP", + "required": [], + "title": "loadBalancerIP", + "type": "string" + }, + "loadBalancerSourceRanges": { + "description": "LoadBalancer source ranges", + "items": { + "required": [] + }, + "required": [], + "title": "loadBalancerSourceRanges", + "type": "array" + }, + "nodePorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": "", + "description": "Node port (\"\" = auto-assign)", + "required": [], + "title": "http", + "type": "string" + } + }, + "required": [ + "http" + ], + "title": "nodePorts", + "type": "object" + }, + "ports": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3022, + "description": "Service HTTP port", + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "ports", + "type": "object" + }, + "sessionAffinity": { + "default": "None", + "description": "Session affinity", + "required": [], + "title": "sessionAffinity", + "type": "string" + }, + "sessionAffinityConfig": { + "additionalProperties": true, + "description": "Session affinity config", + "required": [], + "title": "sessionAffinityConfig" + }, + "type": { + "default": "ClusterIP", + "description": "Service type", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type", + "ports", + "nodePorts", + "clusterIP", + "loadBalancerIP", + "loadBalancerSourceRanges", + "externalTrafficPolicy", + "sessionAffinity", + "extraPorts" + ], + "title": "service", + "type": "object" + }, + "sidecars": { + "description": "Sidecar containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "sidecars", + "type": "array" + }, + "startupProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the startup probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 24, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 5, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 5, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "startupProbe", + "type": "object" + }, + "terminationGracePeriodSeconds": { + "default": 30, + "description": "Pod termination grace period", + "required": [], + "title": "terminationGracePeriodSeconds", + "type": "integer" + }, + "tolerations": { + "description": "Tolerations", + "items": { + "required": [] + }, + "required": [], + "title": "tolerations", + "type": "array" + }, + "topologySpreadConstraints": { + "description": "Topology spread constraints", + "items": { + "required": [] + }, + "required": [], + "title": "topologySpreadConstraints", + "type": "array" + }, + "updateStrategy": { + "additionalProperties": true, + "description": "Deployment update strategy", + "properties": { + "type": { + "default": "RollingUpdate", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "updateStrategy" + } + }, + "required": [ + "enabled", + "replicaCount", + "containerPorts", + "command", + "args", + "extraEnvVars", + "extraEnvVarsCM", + "extraEnvVarsSecret", + "extraVolumes", + "extraVolumeMounts", + "initContainers", + "sidecars", + "hostAliases", + "terminationGracePeriodSeconds", + "revisionHistoryLimit", + "disableRestartOnChanges", + "startupProbe", + "livenessProbe", + "readinessProbe", + "tolerations", + "podAntiAffinityPreset", + "topologySpreadConstraints", + "priorityClassName", + "schedulerName", + "service", + "ingress", + "route", + "pdb", + "autoscaling", + "networkPolicy" + ], + "title": "accountConsole", + "type": "object" + }, "adminConsole": { + "additionalProperties": false, + "properties": { + "affinity": { + "additionalProperties": true, + "description": "Affinity (overrides the anti-affinity preset when set)", + "required": [], + "title": "affinity" + }, + "args": { + "description": "Override the container args", + "items": { + "required": [] + }, + "required": [], + "title": "args", + "type": "array" + }, + "autoscaling": { + "additionalProperties": false, + "properties": { + "hpa": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": false, + "description": "Enable HPA for the UI", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "maxReplicas": { + "default": 5, + "description": "Maximum replicas", + "required": [], + "title": "maxReplicas", + "type": "integer" + }, + "minReplicas": { + "default": 2, + "description": "Minimum replicas", + "required": [], + "title": "minReplicas", + "type": "integer" + }, + "targetCPU": { + "default": 75, + "description": "Target CPU utilization percentage", + "required": [], + "title": "targetCPU", + "type": "integer" + }, + "targetMemory": { + "default": "", + "description": "Target memory utilization percentage", + "required": [], + "title": "targetMemory", + "type": "string" + } + }, + "required": [ + "enabled", + "minReplicas", + "maxReplicas", + "targetCPU", + "targetMemory" + ], + "title": "hpa", + "type": "object" + } + }, + "required": [ + "hpa" + ], + "title": "autoscaling", + "type": "object" + }, + "command": { + "description": "Override the container command", + "items": { + "required": [] + }, + "required": [], + "title": "command", + "type": "array" + }, + "config": { + "additionalProperties": true, + "description": "Extra environment variables rendered literally into the env ConfigMap", + "required": [], + "title": "config" + }, + "containerPorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3021, + "description": "Admin console listener port", + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "containerPorts", + "type": "object" + }, + "containerSecurityContext": { + "additionalProperties": true, + "description": "Container security context (same root-image caveat as the server)", + "properties": { + "allowPrivilegeEscalation": { + "default": false, + "required": [], + "title": "allowPrivilegeEscalation", + "type": "boolean" + }, + "capabilities": { + "additionalProperties": false, + "properties": { + "drop": { + "items": { + "anyOf": [ + { + "required": [], + "type": "string" + } + ], + "required": [] + }, + "required": [], + "title": "drop", + "type": "array" + } + }, + "required": [ + "drop" + ], + "title": "capabilities", + "type": "object" + }, + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "default": false, + "required": [], + "title": "readOnlyRootFilesystem", + "type": "boolean" + }, + "runAsNonRoot": { + "default": false, + "required": [], + "title": "runAsNonRoot", + "type": "boolean" + }, + "runAsUser": { + "default": 0, + "required": [], + "title": "runAsUser", + "type": "integer" + }, + "seccompProfile": { + "additionalProperties": false, + "properties": { + "type": { + "default": "RuntimeDefault", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "seccompProfile", + "type": "object" + } + }, + "required": [ + "enabled", + "runAsUser", + "runAsNonRoot", + "allowPrivilegeEscalation", + "capabilities", + "seccompProfile", + "readOnlyRootFilesystem" + ], + "title": "containerSecurityContext" + }, + "customLivenessProbe": { + "additionalProperties": true, + "description": "Custom liveness probe", + "required": [], + "title": "customLivenessProbe" + }, + "customReadinessProbe": { + "additionalProperties": true, + "description": "Custom readiness probe", + "required": [], + "title": "customReadinessProbe" + }, + "customStartupProbe": { + "additionalProperties": true, + "description": "Custom startup probe", + "required": [], + "title": "customStartupProbe" + }, + "disableRestartOnChanges": { + "default": false, + "description": "Disable the checksum annotations that roll pods on config changes", + "required": [], + "title": "disableRestartOnChanges", + "type": "boolean" + }, + "enabled": { + "default": true, + "description": "Enable the admin console and deploy it separately in split-console mode", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEnvVars": { + "description": "Extra environment variables for the admin console container", + "items": { + "required": [] + }, + "required": [], + "title": "extraEnvVars", + "type": "array" + }, + "extraEnvVarsCM": { + "default": "", + "description": "Extra ConfigMap with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsCM", + "type": "string" + }, + "extraEnvVarsSecret": { + "default": "", + "description": "Extra Secret with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsSecret", + "type": "string" + }, + "extraVolumeMounts": { + "description": "Extra volume mounts (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumeMounts", + "type": "array" + }, + "extraVolumes": { + "description": "Extra volumes (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumes", + "type": "array" + }, + "hostAliases": { + "description": "Pod host aliases", + "items": { + "required": [] + }, + "required": [], + "title": "hostAliases", + "type": "array" + }, + "ingress": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "Ingress annotations (tpl-rendered)", + "required": [], + "title": "annotations" + }, + "certManager": { + "default": false, + "description": "Request a cert-manager certificate (adds kubernetes.io/tls-acme)", + "required": [], + "title": "certManager", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Enable ingress-nginx routing for the split admin console", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraHosts": { + "description": "Extra hosts", + "items": { + "required": [] + }, + "required": [], + "title": "extraHosts", + "type": "array" + }, + "extraPaths": { + "description": "Extra paths for the primary host", + "items": { + "required": [] + }, + "required": [], + "title": "extraPaths", + "type": "array" + }, + "extraRules": { + "description": "Full custom rules (tpl-rendered; appended after the generated rules)", + "items": { + "required": [] + }, + "required": [], + "title": "extraRules", + "type": "array" + }, + "extraTls": { + "description": "Extra TLS entries", + "items": { + "required": [] + }, + "required": [], + "title": "extraTls", + "type": "array" + }, + "hostname": { + "default": "", + "description": "Ingress hostname (tpl-rendered); also drives the derived UI public URL", + "required": [], + "title": "hostname", + "type": "string" + }, + "ingressClassName": { + "default": "", + "description": "Ingress class name", + "required": [], + "title": "ingressClassName", + "type": "string" + }, + "path": { + "default": "/console/admin", + "description": "Public console path (the generated ingress strips it)", + "required": [], + "title": "path", + "type": "string" + }, + "pathType": { + "default": "Prefix", + "description": "Ingress path type", + "required": [], + "title": "pathType", + "type": "string" + }, + "tls": { + "default": false, + "description": "Enable TLS for the hostname", + "required": [], + "title": "tls", + "type": "boolean" + } + }, + "required": [ + "enabled", + "hostname", + "path", + "pathType", + "ingressClassName", + "tls", + "certManager", + "extraHosts", + "extraPaths", + "extraTls", + "extraRules" + ], + "title": "ingress", + "type": "object" + }, + "initContainers": { + "description": "Init containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "initContainers", + "type": "array" + }, + "lifecycleHooks": { + "additionalProperties": true, + "description": "Container lifecycle hooks", + "required": [], + "title": "lifecycleHooks" + }, + "livenessProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the liveness probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 3, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 0, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 30, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "livenessProbe", + "type": "object" + }, + "networkPolicy": { + "additionalProperties": false, + "properties": { + "allowExternal": { + "default": true, + "description": "Allow ingress from anywhere", + "required": [], + "title": "allowExternal", + "type": "boolean" + }, + "allowExternalEgress": { + "default": true, + "description": "Allow all egress", + "required": [], + "title": "allowExternalEgress", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Create a NetworkPolicy for the UI", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEgress": { + "description": "Extra egress rules", + "items": { + "required": [] + }, + "required": [], + "title": "extraEgress", + "type": "array" + }, + "extraIngress": { + "description": "Extra ingress rules", + "items": { + "required": [] + }, + "required": [], + "title": "extraIngress", + "type": "array" + }, + "ingressNSMatchLabels": { + "additionalProperties": true, + "description": "Namespace labels allowed to connect when allowExternal is false", + "required": [], + "title": "ingressNSMatchLabels" + }, + "ingressPodMatchLabels": { + "additionalProperties": true, + "description": "Pod labels allowed to connect when allowExternal is false", + "required": [], + "title": "ingressPodMatchLabels" + } + }, + "required": [ + "enabled", + "allowExternal", + "allowExternalEgress", + "extraIngress", + "extraEgress" + ], + "title": "networkPolicy", + "type": "object" + }, + "nodeSelector": { + "additionalProperties": true, + "description": "Node selector", + "required": [], + "title": "nodeSelector" + }, + "pdb": { + "additionalProperties": false, + "properties": { + "create": { + "default": false, + "description": "Create a PodDisruptionBudget for the UI", + "required": [], + "title": "create", + "type": "boolean" + }, + "maxUnavailable": { + "default": "", + "description": "Maximum unavailable pods (defaults to 1 when both are empty)", + "required": [], + "title": "maxUnavailable", + "type": "string" + }, + "minAvailable": { + "default": "", + "description": "Minimum available pods", + "required": [], + "title": "minAvailable", + "type": "string" + } + }, + "required": [ + "create", + "minAvailable", + "maxUnavailable" + ], + "title": "pdb", + "type": "object" + }, + "podAnnotations": { + "additionalProperties": true, + "description": "Pod annotations (tpl-rendered)", + "required": [], + "title": "podAnnotations" + }, + "podAntiAffinityPreset": { + "default": "soft", + "description": "Pod anti-affinity preset: soft, hard or \"\"", + "required": [], + "title": "podAntiAffinityPreset", + "type": "string" + }, + "podLabels": { + "additionalProperties": true, + "description": "Pod labels (tpl-rendered)", + "required": [], + "title": "podLabels" + }, + "podSecurityContext": { + "additionalProperties": true, + "description": "Pod security context", + "properties": { + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "fsGroup": { + "default": 1000, + "required": [], + "title": "fsGroup", + "type": "integer" + } + }, + "required": [ + "enabled", + "fsGroup" + ], + "title": "podSecurityContext" + }, + "priorityClassName": { + "default": "", + "description": "Priority class name", + "required": [], + "title": "priorityClassName", + "type": "string" + }, + "readinessProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the readiness probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 3, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 0, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 10, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "readinessProbe", + "type": "object" + }, + "replicaCount": { + "default": 1, + "description": "Number of admin console replicas", + "required": [], + "title": "replicaCount", + "type": "integer" + }, + "resources": { + "additionalProperties": true, + "description": "Admin console container resources", + "properties": { + "limits": { + "additionalProperties": true, + "properties": { + "memory": { + "default": "512Mi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "memory" + ], + "title": "limits" + }, + "requests": { + "additionalProperties": true, + "properties": { + "cpu": { + "default": "100m", + "required": [], + "title": "cpu", + "type": "string" + }, + "memory": { + "default": "256Mi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "cpu", + "memory" + ], + "title": "requests" + } + }, + "required": [], + "title": "resources" + }, + "revisionHistoryLimit": { + "default": 3, + "description": "Deployment revision history limit", + "required": [], + "title": "revisionHistoryLimit", + "type": "integer" + }, + "route": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "HTTPRoute annotations", + "required": [], + "title": "annotations" + }, + "enabled": { + "default": false, + "description": "Create a Gateway API HTTPRoute for the admin console (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": [ + "boolean", + "string" + ] + }, + "filters": { + "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", + "items": { + "required": [] + }, + "required": [], + "title": "filters", + "type": "array" + }, + "hostnames": { + "description": "Route hostnames ([] = derived from server.publicUrl)", + "items": { + "required": [] + }, + "required": [], + "title": "hostnames", + "type": "array" + }, + "matches": { + "description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"", + "items": { + "required": [] + }, + "required": [], + "title": "matches", + "type": "array" + }, + "parentRefs": { + "description": "Gateway parentRefs", + "items": { + "required": [] + }, + "required": [], + "title": "parentRefs", + "type": "array" + } + }, + "required": [ + "enabled", + "hostnames", + "parentRefs", + "matches", + "filters" + ], + "title": "route", + "type": "object" + }, + "schedulerName": { + "default": "", + "description": "Scheduler name", + "required": [], + "title": "schedulerName", + "type": "string" + }, + "service": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "Service annotations (tpl-rendered)", + "required": [], + "title": "annotations" + }, + "clusterIP": { + "default": "", + "description": "Static cluster IP", + "required": [], + "title": "clusterIP", + "type": "string" + }, + "externalTrafficPolicy": { + "default": "Cluster", + "description": "External traffic policy", + "required": [], + "title": "externalTrafficPolicy", + "type": "string" + }, + "extraPorts": { + "description": "Extra service ports", + "items": { + "required": [] + }, + "required": [], + "title": "extraPorts", + "type": "array" + }, + "loadBalancerIP": { + "default": "", + "description": "LoadBalancer IP", + "required": [], + "title": "loadBalancerIP", + "type": "string" + }, + "loadBalancerSourceRanges": { + "description": "LoadBalancer source ranges", + "items": { + "required": [] + }, + "required": [], + "title": "loadBalancerSourceRanges", + "type": "array" + }, + "nodePorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": "", + "description": "Node port (\"\" = auto-assign)", + "required": [], + "title": "http", + "type": "string" + } + }, + "required": [ + "http" + ], + "title": "nodePorts", + "type": "object" + }, + "ports": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3021, + "description": "Service HTTP port", + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "ports", + "type": "object" + }, + "sessionAffinity": { + "default": "None", + "description": "Session affinity", + "required": [], + "title": "sessionAffinity", + "type": "string" + }, + "sessionAffinityConfig": { + "additionalProperties": true, + "description": "Session affinity config", + "required": [], + "title": "sessionAffinityConfig" + }, + "type": { + "default": "ClusterIP", + "description": "Service type", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type", + "ports", + "nodePorts", + "clusterIP", + "loadBalancerIP", + "loadBalancerSourceRanges", + "externalTrafficPolicy", + "sessionAffinity", + "extraPorts" + ], + "title": "service", + "type": "object" + }, + "sidecars": { + "description": "Sidecar containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "sidecars", + "type": "array" + }, + "startupProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Enable the startup probe", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "failureThreshold": { + "default": 24, + "required": [], + "title": "failureThreshold", + "type": "integer" + }, + "initialDelaySeconds": { + "default": 5, + "required": [], + "title": "initialDelaySeconds", + "type": "integer" + }, + "periodSeconds": { + "default": 5, + "required": [], + "title": "periodSeconds", + "type": "integer" + }, + "successThreshold": { + "default": 1, + "required": [], + "title": "successThreshold", + "type": "integer" + }, + "timeoutSeconds": { + "default": 5, + "required": [], + "title": "timeoutSeconds", + "type": "integer" + } + }, + "required": [ + "enabled", + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "failureThreshold", + "successThreshold" + ], + "title": "startupProbe", + "type": "object" + }, + "terminationGracePeriodSeconds": { + "default": 30, + "description": "Pod termination grace period", + "required": [], + "title": "terminationGracePeriodSeconds", + "type": "integer" + }, + "tolerations": { + "description": "Tolerations", + "items": { + "required": [] + }, + "required": [], + "title": "tolerations", + "type": "array" + }, + "topologySpreadConstraints": { + "description": "Topology spread constraints", + "items": { + "required": [] + }, + "required": [], + "title": "topologySpreadConstraints", + "type": "array" + }, + "updateStrategy": { + "additionalProperties": true, + "description": "Deployment update strategy", + "properties": { + "type": { + "default": "RollingUpdate", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "updateStrategy" + } + }, + "required": [ + "enabled", + "replicaCount", + "containerPorts", + "command", + "args", + "extraEnvVars", + "extraEnvVarsCM", + "extraEnvVarsSecret", + "extraVolumes", + "extraVolumeMounts", + "initContainers", + "sidecars", + "hostAliases", + "terminationGracePeriodSeconds", + "revisionHistoryLimit", + "disableRestartOnChanges", + "startupProbe", + "livenessProbe", + "readinessProbe", + "tolerations", + "podAntiAffinityPreset", + "topologySpreadConstraints", + "priorityClassName", + "schedulerName", + "service", + "ingress", + "route", + "pdb", + "autoscaling", + "networkPolicy" + ], + "title": "adminConsole", + "type": "object" + }, + "auth": { + "additionalProperties": false, + "description": "#\n# Credentials seeded into authup at boot. Empty passwords are generated once and\n# kept stable across upgrades via lookup.\n# GITOPS WARNING: lookup is inert under `helm template` and ArgoCD-style\n# renders — every sync would then apply a NEW random value, roll the pods via\n# the checksum annotation, and desync the Secret from the live credential\n# (authup only applies the admin password at first provisioning unless the\n# reset flag is set). GitOps users MUST set explicit values or auth.existingSecret.", + "properties": { + "adminPassword": { + "default": "", + "description": "Initial admin user password (\"\" = generate once, keep across upgrades).\nChanging it after the first install only takes effect with\nadminPasswordReset=true for one upgrade cycle", + "required": [], + "title": "adminPassword", + "type": "string" + }, + "adminPasswordReset": { + "default": false, + "description": "Re-assert the admin password on every boot (USER_ADMIN_PASSWORD_RESET)", + "required": [], + "title": "adminPasswordReset", + "type": "boolean" + }, + "existingSecret": { + "default": "", + "description": "Existing secret holding the keys below instead of the chart-managed secret (tpl-rendered)", + "required": [], + "title": "existingSecret", + "type": "string" + }, + "secretKeys": { + "additionalProperties": false, + "properties": { + "adminPasswordKey": { + "default": "admin-password", + "description": "Key inside the (existing) secret holding the admin password", + "required": [], + "title": "adminPasswordKey", + "type": "string" + }, + "secretsEncryptionKeyKey": { + "default": "secrets-encryption-key", + "description": "Key inside the (existing) secret holding the secrets encryption key", + "required": [], + "title": "secretsEncryptionKeyKey", + "type": "string" + }, + "systemClientSecretKey": { + "default": "system-client-secret", + "description": "Key inside the (existing) secret holding the system client secret", + "required": [], + "title": "systemClientSecretKey", + "type": "string" + } + }, + "required": [ + "adminPasswordKey", + "systemClientSecretKey", + "secretsEncryptionKeyKey" + ], + "title": "secretKeys", + "type": "object" + }, + "secretsEncryptionKey": { + "default": "", + "description": "Optional AES-256 key-encryption-key wrapping the realm key store\n(SECRETS_ENCRYPTION_KEY, base64-encoded 32 bytes). Deliberately never\ngenerated by the chart: this key is effectively write-once — losing or\nrotating it bricks wrapped MFA seeds and signing keys. Set it explicitly\nhere, or reference it via existingSecret plus\nsecretsEncryptionKeyEnabled=true, and back it up.", + "required": [], + "title": "secretsEncryptionKey", + "type": "string" + }, + "secretsEncryptionKeyEnabled": { + "default": false, + "description": "Declare that auth.existingSecret carries the secrets encryption key\n(under secretKeys.secretsEncryptionKeyKey). Required for the key to be\nwired from an existing secret — it is never inferred, so a missing key can\nnot silently fail open into plaintext-at-rest", + "required": [], + "title": "secretsEncryptionKeyEnabled", + "type": "boolean" + }, + "systemClientEnabled": { + "default": false, + "description": "Provision the built-in system client (CLIENT_SYSTEM_ENABLED); required for machine-to-machine consumers", + "required": [], + "title": "systemClientEnabled", + "type": "boolean" + }, + "systemClientSecret": { + "default": "", + "description": "System client secret (\"\" = generate once when systemClientEnabled)", + "required": [], + "title": "systemClientSecret", + "type": "string" + }, + "systemClientSecretReset": { + "default": false, + "description": "Re-assert the system client secret on every boot (CLIENT_SYSTEM_SECRET_RESET)", + "required": [], + "title": "systemClientSecretReset", + "type": "boolean" + } + }, + "required": [ + "adminPassword", + "adminPasswordReset", + "systemClientEnabled", + "systemClientSecret", + "systemClientSecretReset", + "secretsEncryptionKey", + "secretsEncryptionKeyEnabled", + "existingSecret", + "secretKeys" + ], + "title": "auth", + "type": "object" + }, + "authConsole": { "additionalProperties": false, "description": "#", "properties": { @@ -12,13 +2214,6 @@ "required": [], "title": "affinity" }, - "apiUrl": { - "default": "", - "description": "Browser-facing server-core URL (NUXT_PUBLIC_API_URL). \"\" = the server public URL.\nMust be reachable from the user's browser, never a cluster-internal DNS name", - "required": [], - "title": "apiUrl", - "type": "string" - }, "args": { "description": "Override the container args", "items": { @@ -102,6 +2297,23 @@ "required": [], "title": "config" }, + "containerPorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3020, + "description": "Auth console listener port", + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "containerPorts", + "type": "object" + }, "containerSecurityContext": { "additionalProperties": true, "description": "Container security context (same root-image caveat as the server)", @@ -215,13 +2427,13 @@ }, "enabled": { "default": true, - "description": "Deploy the client-admin-console admin UI (false = headless IdP)", + "description": "Enable the auth console and deploy it separately in split-console mode", "required": [], "title": "enabled", "type": "boolean" }, "extraEnvVars": { - "description": "Extra environment variables for the UI container", + "description": "Extra environment variables for the auth console container", "items": { "required": [] }, @@ -288,7 +2500,7 @@ }, "enabled": { "default": false, - "description": "Enable ingress for the UI", + "description": "Enable ingress-nginx routing for the split auth console", "required": [], "title": "enabled", "type": "boolean" @@ -344,8 +2556,8 @@ "type": "string" }, "path": { - "default": "/", - "description": "Ingress path", + "default": "/console/auth", + "description": "Public console path (the generated ingress strips it)", "required": [], "title": "path", "type": "string" @@ -390,13 +2602,6 @@ "title": "initContainers", "type": "array" }, - "internalApiUrl": { - "default": "", - "description": "Server-side (SSR) API URL override (NUXT_API_URL), e.g. the in-cluster\nservice URL to keep SSR traffic off the ingress", - "required": [], - "title": "internalApiUrl", - "type": "string" - }, "lifecycleHooks": { "additionalProperties": true, "description": "Container lifecycle hooks", @@ -608,13 +2813,6 @@ "title": "priorityClassName", "type": "string" }, - "publicUrl": { - "default": "", - "description": "Public URL of the UI (NUXT_PUBLIC_PUBLIC_URL). \"\" = derived from adminConsole.ingress", - "required": [], - "title": "publicUrl", - "type": "string" - }, "readinessProbe": { "additionalProperties": false, "properties": { @@ -669,14 +2867,14 @@ }, "replicaCount": { "default": 1, - "description": "Number of UI replicas (fully stateless, scale freely)", + "description": "Number of auth console replicas", "required": [], "title": "replicaCount", "type": "integer" }, "resources": { "additionalProperties": true, - "description": "UI container resources", + "description": "Auth console container resources", "properties": { "limits": { "additionalProperties": true, @@ -737,7 +2935,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;\n\"false\" and \"\" disable it, anything else fails the render)", + "description": "Create a Gateway API HTTPRoute for the auth console (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": [ @@ -755,7 +2953,7 @@ "type": "array" }, "hostnames": { - "description": "Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public\nURL path is dropped and needs its own matches entry)", + "description": "Route hostnames ([] = derived from server.publicUrl)", "items": { "required": [] }, @@ -868,8 +3066,8 @@ "additionalProperties": false, "properties": { "http": { - "default": 3000, - "description": "Service HTTP port (the container port is fixed at 3000)", + "default": 3020, + "description": "Service HTTP port", "required": [], "title": "http", "type": "integer" @@ -1022,9 +3220,7 @@ "required": [ "enabled", "replicaCount", - "publicUrl", - "apiUrl", - "internalApiUrl", + "containerPorts", "command", "args", "extraEnvVars", @@ -1053,115 +3249,7 @@ "autoscaling", "networkPolicy" ], - "title": "adminConsole", - "type": "object" - }, - "auth": { - "additionalProperties": false, - "description": "#\n# Credentials seeded into authup at boot. Empty passwords are generated once and\n# kept stable across upgrades via lookup.\n# GITOPS WARNING: lookup is inert under `helm template` and ArgoCD-style\n# renders — every sync would then apply a NEW random value, roll the pods via\n# the checksum annotation, and desync the Secret from the live credential\n# (authup only applies the admin password at first provisioning unless the\n# reset flag is set). GitOps users MUST set explicit values or auth.existingSecret.", - "properties": { - "adminPassword": { - "default": "", - "description": "Initial admin user password (\"\" = generate once, keep across upgrades).\nChanging it after the first install only takes effect with\nadminPasswordReset=true for one upgrade cycle", - "required": [], - "title": "adminPassword", - "type": "string" - }, - "adminPasswordReset": { - "default": false, - "description": "Re-assert the admin password on every boot (USER_ADMIN_PASSWORD_RESET)", - "required": [], - "title": "adminPasswordReset", - "type": "boolean" - }, - "existingSecret": { - "default": "", - "description": "Existing secret holding the keys below instead of the chart-managed secret (tpl-rendered)", - "required": [], - "title": "existingSecret", - "type": "string" - }, - "secretKeys": { - "additionalProperties": false, - "properties": { - "adminPasswordKey": { - "default": "admin-password", - "description": "Key inside the (existing) secret holding the admin password", - "required": [], - "title": "adminPasswordKey", - "type": "string" - }, - "secretsEncryptionKeyKey": { - "default": "secrets-encryption-key", - "description": "Key inside the (existing) secret holding the secrets encryption key", - "required": [], - "title": "secretsEncryptionKeyKey", - "type": "string" - }, - "systemClientSecretKey": { - "default": "system-client-secret", - "description": "Key inside the (existing) secret holding the system client secret", - "required": [], - "title": "systemClientSecretKey", - "type": "string" - } - }, - "required": [ - "adminPasswordKey", - "systemClientSecretKey", - "secretsEncryptionKeyKey" - ], - "title": "secretKeys", - "type": "object" - }, - "secretsEncryptionKey": { - "default": "", - "description": "Optional AES-256 key-encryption-key wrapping the realm key store\n(SECRETS_ENCRYPTION_KEY, base64-encoded 32 bytes). Deliberately never\ngenerated by the chart: this key is effectively write-once — losing or\nrotating it bricks wrapped MFA seeds and signing keys. Set it explicitly\nhere, or reference it via existingSecret plus\nsecretsEncryptionKeyEnabled=true, and back it up.", - "required": [], - "title": "secretsEncryptionKey", - "type": "string" - }, - "secretsEncryptionKeyEnabled": { - "default": false, - "description": "Declare that auth.existingSecret carries the secrets encryption key\n(under secretKeys.secretsEncryptionKeyKey). Required for the key to be\nwired from an existing secret — it is never inferred, so a missing key can\nnot silently fail open into plaintext-at-rest", - "required": [], - "title": "secretsEncryptionKeyEnabled", - "type": "boolean" - }, - "systemClientEnabled": { - "default": false, - "description": "Provision the built-in system client (CLIENT_SYSTEM_ENABLED); required for machine-to-machine consumers", - "required": [], - "title": "systemClientEnabled", - "type": "boolean" - }, - "systemClientSecret": { - "default": "", - "description": "System client secret (\"\" = generate once when systemClientEnabled)", - "required": [], - "title": "systemClientSecret", - "type": "string" - }, - "systemClientSecretReset": { - "default": false, - "description": "Re-assert the system client secret on every boot (CLIENT_SYSTEM_SECRET_RESET)", - "required": [], - "title": "systemClientSecretReset", - "type": "boolean" - } - }, - "required": [ - "adminPassword", - "adminPasswordReset", - "systemClientEnabled", - "systemClientSecret", - "systemClientSecretReset", - "secretsEncryptionKey", - "secretsEncryptionKeyEnabled", - "existingSecret", - "secretKeys" - ], - "title": "auth", + "title": "authConsole", "type": "object" }, "commonAnnotations": { @@ -2404,11 +4492,14 @@ "additionalProperties": false, "properties": { "accountConsole": { - "default": true, - "description": "Serve the account self-service console at \u003cpublicUrl\u003e/account\n(profile, password, authenticators, sessions, applications).\nACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal", + "default": "", + "description": "Deprecated: use accountConsole.enabled. Any non-empty value fails.", "required": [], "title": "accountConsole", - "type": "boolean" + "type": [ + "boolean", + "string" + ] }, "emailVerification": { "default": false, @@ -3301,6 +5392,13 @@ "title": "sidecars", "type": "array" }, + "splitConsoles": { + "default": false, + "description": "Run API-only server pods and deploy the console workloads separately", + "required": [], + "title": "splitConsoles", + "type": "boolean" + }, "startupProbe": { "additionalProperties": false, "properties": { @@ -3521,6 +5619,7 @@ }, "required": [ "enabled", + "splitConsoles", "replicaCount", "publicUrl", "trustedOrigins", @@ -3952,7 +6051,9 @@ "externalRedis", "smtp", "server", + "authConsole", "adminConsole", + "accountConsole", "serviceAccount" ], "type": "object" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 83e654d..2bb4bc3 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -403,6 +403,8 @@ smtp: server: # -- Deploy the server-core service enabled: true + # -- Run API-only server pods and deploy the console workloads separately + splitConsoles: false # -- Number of server-core replicas (values > 1 REQUIRE a configured cache) replicaCount: 1 # -- Public URL of server-core (PUBLIC_URL) — the OIDC issuer origin. @@ -429,10 +431,12 @@ server: passwordRecovery: false # -- Enable email verification (EMAIL_VERIFICATION_ENABLED; requires SMTP) emailVerification: false - # -- Serve the account self-service console at /account - # (profile, password, authenticators, sessions, applications). - # ACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal - accountConsole: true + # @schema + # type: [boolean, string] + # required: true + # @schema + # -- Deprecated: use accountConsole.enabled. Any non-empty value fails. + accountConsole: "" mfa: # -- Enable multi-factor authentication (MFA_ENABLED) enabled: false @@ -838,21 +842,16 @@ server: # -- Extra egress rules extraEgress: [] -## @section client-admin-console (admin UI) +## @section Split console workloads -adminConsole: - # -- Deploy the client-admin-console admin UI (false = headless IdP) +authConsole: + # -- Enable the auth console and deploy it separately in split-console mode enabled: true - # -- Number of UI replicas (fully stateless, scale freely) + # -- Number of auth console replicas replicaCount: 1 - # -- Public URL of the UI (NUXT_PUBLIC_PUBLIC_URL). "" = derived from adminConsole.ingress - publicUrl: "" - # -- Browser-facing server-core URL (NUXT_PUBLIC_API_URL). "" = the server public URL. - # Must be reachable from the user's browser, never a cluster-internal DNS name - apiUrl: "" - # -- Server-side (SSR) API URL override (NUXT_API_URL), e.g. the in-cluster - # service URL to keep SSR traffic off the ingress - internalApiUrl: "" + containerPorts: + # -- Auth console listener port + http: 3020 # @schema # additionalProperties: true # @schema @@ -862,7 +861,7 @@ adminConsole: command: [] # -- Override the container args args: [] - # -- Extra environment variables for the UI container + # -- Extra environment variables for the auth console container extraEnvVars: [] # -- Extra ConfigMap with environment variables (tpl-rendered name) extraEnvVarsCM: "" @@ -929,7 +928,7 @@ adminConsole: # @schema # additionalProperties: true # @schema - # -- UI container resources + # -- Auth console container resources resources: # @schema # additionalProperties: true @@ -1005,8 +1004,8 @@ adminConsole: # -- Service type type: ClusterIP ports: - # -- Service HTTP port (the container port is fixed at 3000) - http: 3000 + # -- Service HTTP port + http: 3020 nodePorts: # -- Node port ("" = auto-assign) http: "" @@ -1033,12 +1032,12 @@ adminConsole: # -- Extra service ports extraPorts: [] ingress: - # -- Enable ingress for the UI + # -- Enable ingress-nginx routing for the split auth console enabled: false # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL hostname: "" - # -- Ingress path - path: / + # -- Public console path (the generated ingress strips it) + path: /console/auth # -- Ingress path type pathType: Prefix # -- Ingress class name @@ -1065,12 +1064,569 @@ adminConsole: # type: [boolean, string] # required: true # @schema - # -- Create a Gateway API HTTPRoute for the UI (tpl-rendered: a string rendering to + # -- Create a Gateway API HTTPRoute for the auth console (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) 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) + # -- Route hostnames ([] = derived from server.publicUrl) + hostnames: [] + # -- Gateway parentRefs + parentRefs: [] + # @schema + # additionalProperties: true + # @schema + # -- HTTPRoute annotations + annotations: {} + # -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" + matches: [] + # -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix + filters: [] + pdb: + # -- Create a PodDisruptionBudget for the UI + create: false + # -- Minimum available pods + minAvailable: "" + # -- Maximum unavailable pods (defaults to 1 when both are empty) + maxUnavailable: "" + autoscaling: + hpa: + # -- Enable HPA for the UI + enabled: false + # -- Minimum replicas + minReplicas: 2 + # -- Maximum replicas + maxReplicas: 5 + # -- Target CPU utilization percentage + targetCPU: 75 + # -- Target memory utilization percentage + targetMemory: "" + networkPolicy: + # -- Create a NetworkPolicy for the UI + enabled: false + # -- Allow ingress from anywhere + allowExternal: true + # -- Allow all egress + allowExternalEgress: true + # @schema + # additionalProperties: true + # @schema + # -- Pod labels allowed to connect when allowExternal is false + ingressPodMatchLabels: {} + # @schema + # additionalProperties: true + # @schema + # -- Namespace labels allowed to connect when allowExternal is false + ingressNSMatchLabels: {} + # -- Extra ingress rules + extraIngress: [] + # -- Extra egress rules + extraEgress: [] + + +adminConsole: + # -- Enable the admin console and deploy it separately in split-console mode + enabled: true + # -- Number of admin console replicas + replicaCount: 1 + containerPorts: + # -- Admin console listener port + http: 3021 + # @schema + # additionalProperties: true + # @schema + # -- Extra environment variables rendered literally into the env ConfigMap + config: {} + # -- Override the container command + command: [] + # -- Override the container args + args: [] + # -- Extra environment variables for the admin console container + extraEnvVars: [] + # -- Extra ConfigMap with environment variables (tpl-rendered name) + extraEnvVarsCM: "" + # -- Extra Secret with environment variables (tpl-rendered name) + extraEnvVarsSecret: "" + # -- Extra volumes (tpl-rendered) + extraVolumes: [] + # -- Extra volume mounts (tpl-rendered) + extraVolumeMounts: [] + # -- Init containers (tpl-rendered) + initContainers: [] + # -- Sidecar containers (tpl-rendered) + sidecars: [] + # @schema + # additionalProperties: true + # @schema + # -- Container lifecycle hooks + lifecycleHooks: {} + # -- Pod host aliases + hostAliases: [] + # -- Pod termination grace period + terminationGracePeriodSeconds: 30 + # @schema + # additionalProperties: true + # @schema + # -- Deployment update strategy + updateStrategy: + type: RollingUpdate + # -- Deployment revision history limit + revisionHistoryLimit: 3 + # @schema + # additionalProperties: true + # @schema + # -- Pod annotations (tpl-rendered) + podAnnotations: {} + # @schema + # additionalProperties: true + # @schema + # -- Pod labels (tpl-rendered) + podLabels: {} + # -- Disable the checksum annotations that roll pods on config changes + disableRestartOnChanges: false + # @schema + # additionalProperties: true + # @schema + # -- Pod security context + podSecurityContext: + enabled: true + fsGroup: 1000 + # @schema + # additionalProperties: true + # @schema + # -- Container security context (same root-image caveat as the server) + containerSecurityContext: + enabled: true + runAsUser: 0 + runAsNonRoot: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + readOnlyRootFilesystem: false + # @schema + # additionalProperties: true + # @schema + # -- Admin console container resources + resources: + # @schema + # additionalProperties: true + # @schema + requests: + cpu: 100m + memory: 256Mi + # @schema + # additionalProperties: true + # @schema + limits: + memory: 512Mi + startupProbe: + # -- Enable the startup probe + enabled: true + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 24 + successThreshold: 1 + livenessProbe: + # -- Enable the liveness probe + enabled: true + initialDelaySeconds: 0 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + readinessProbe: + # -- Enable the readiness probe + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + # @schema + # additionalProperties: true + # @schema + # -- Custom startup probe + customStartupProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Custom liveness probe + customLivenessProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Custom readiness probe + customReadinessProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: [] + # @schema + # additionalProperties: true + # @schema + # -- Affinity (overrides the anti-affinity preset when set) + affinity: {} + # -- Pod anti-affinity preset: soft, hard or "" + podAntiAffinityPreset: soft + # -- Topology spread constraints + topologySpreadConstraints: [] + # -- Priority class name + priorityClassName: "" + # -- Scheduler name + schedulerName: "" + service: + # -- Service type + type: ClusterIP + ports: + # -- Service HTTP port + http: 3021 + nodePorts: + # -- Node port ("" = auto-assign) + http: "" + # -- Static cluster IP + clusterIP: "" + # -- LoadBalancer IP + loadBalancerIP: "" + # -- LoadBalancer source ranges + loadBalancerSourceRanges: [] + # -- External traffic policy + externalTrafficPolicy: Cluster + # -- Session affinity + sessionAffinity: None + # @schema + # additionalProperties: true + # @schema + # -- Session affinity config + sessionAffinityConfig: {} + # @schema + # additionalProperties: true + # @schema + # -- Service annotations (tpl-rendered) + annotations: {} + # -- Extra service ports + extraPorts: [] + ingress: + # -- Enable ingress-nginx routing for the split admin console + enabled: false + # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL + hostname: "" + # -- Public console path (the generated ingress strips it) + path: /console/admin + # -- Ingress path type + pathType: Prefix + # -- Ingress class name + ingressClassName: "" + # @schema + # additionalProperties: true + # @schema + # -- Ingress annotations (tpl-rendered) + annotations: {} + # -- Enable TLS for the hostname + tls: false + # -- Request a cert-manager certificate (adds kubernetes.io/tls-acme) + certManager: false + # -- Extra hosts + extraHosts: [] + # -- Extra paths for the primary host + extraPaths: [] + # -- Extra TLS entries + extraTls: [] + # -- Full custom rules (tpl-rendered; appended after the generated rules) + extraRules: [] + route: + # @schema + # type: [boolean, string] + # required: true + # @schema + # -- Create a Gateway API HTTPRoute for the admin console (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) + enabled: false + # -- Route hostnames ([] = derived from server.publicUrl) + hostnames: [] + # -- Gateway parentRefs + parentRefs: [] + # @schema + # additionalProperties: true + # @schema + # -- HTTPRoute annotations + annotations: {} + # -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" + matches: [] + # -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix + filters: [] + pdb: + # -- Create a PodDisruptionBudget for the UI + create: false + # -- Minimum available pods + minAvailable: "" + # -- Maximum unavailable pods (defaults to 1 when both are empty) + maxUnavailable: "" + autoscaling: + hpa: + # -- Enable HPA for the UI + enabled: false + # -- Minimum replicas + minReplicas: 2 + # -- Maximum replicas + maxReplicas: 5 + # -- Target CPU utilization percentage + targetCPU: 75 + # -- Target memory utilization percentage + targetMemory: "" + networkPolicy: + # -- Create a NetworkPolicy for the UI + enabled: false + # -- Allow ingress from anywhere + allowExternal: true + # -- Allow all egress + allowExternalEgress: true + # @schema + # additionalProperties: true + # @schema + # -- Pod labels allowed to connect when allowExternal is false + ingressPodMatchLabels: {} + # @schema + # additionalProperties: true + # @schema + # -- Namespace labels allowed to connect when allowExternal is false + ingressNSMatchLabels: {} + # -- Extra ingress rules + extraIngress: [] + # -- Extra egress rules + extraEgress: [] + + +accountConsole: + # -- Enable the account console and deploy it separately in split-console mode + enabled: true + # -- Number of account console replicas + replicaCount: 1 + containerPorts: + # -- Account console listener port + http: 3022 + # @schema + # additionalProperties: true + # @schema + # -- Extra environment variables rendered literally into the env ConfigMap + config: {} + # -- Override the container command + command: [] + # -- Override the container args + args: [] + # -- Extra environment variables for the account console container + extraEnvVars: [] + # -- Extra ConfigMap with environment variables (tpl-rendered name) + extraEnvVarsCM: "" + # -- Extra Secret with environment variables (tpl-rendered name) + extraEnvVarsSecret: "" + # -- Extra volumes (tpl-rendered) + extraVolumes: [] + # -- Extra volume mounts (tpl-rendered) + extraVolumeMounts: [] + # -- Init containers (tpl-rendered) + initContainers: [] + # -- Sidecar containers (tpl-rendered) + sidecars: [] + # @schema + # additionalProperties: true + # @schema + # -- Container lifecycle hooks + lifecycleHooks: {} + # -- Pod host aliases + hostAliases: [] + # -- Pod termination grace period + terminationGracePeriodSeconds: 30 + # @schema + # additionalProperties: true + # @schema + # -- Deployment update strategy + updateStrategy: + type: RollingUpdate + # -- Deployment revision history limit + revisionHistoryLimit: 3 + # @schema + # additionalProperties: true + # @schema + # -- Pod annotations (tpl-rendered) + podAnnotations: {} + # @schema + # additionalProperties: true + # @schema + # -- Pod labels (tpl-rendered) + podLabels: {} + # -- Disable the checksum annotations that roll pods on config changes + disableRestartOnChanges: false + # @schema + # additionalProperties: true + # @schema + # -- Pod security context + podSecurityContext: + enabled: true + fsGroup: 1000 + # @schema + # additionalProperties: true + # @schema + # -- Container security context (same root-image caveat as the server) + containerSecurityContext: + enabled: true + runAsUser: 0 + runAsNonRoot: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + readOnlyRootFilesystem: false + # @schema + # additionalProperties: true + # @schema + # -- Account console container resources + resources: + # @schema + # additionalProperties: true + # @schema + requests: + cpu: 100m + memory: 256Mi + # @schema + # additionalProperties: true + # @schema + limits: + memory: 512Mi + startupProbe: + # -- Enable the startup probe + enabled: true + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 24 + successThreshold: 1 + livenessProbe: + # -- Enable the liveness probe + enabled: true + initialDelaySeconds: 0 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + readinessProbe: + # -- Enable the readiness probe + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + # @schema + # additionalProperties: true + # @schema + # -- Custom startup probe + customStartupProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Custom liveness probe + customLivenessProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Custom readiness probe + customReadinessProbe: {} + # @schema + # additionalProperties: true + # @schema + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: [] + # @schema + # additionalProperties: true + # @schema + # -- Affinity (overrides the anti-affinity preset when set) + affinity: {} + # -- Pod anti-affinity preset: soft, hard or "" + podAntiAffinityPreset: soft + # -- Topology spread constraints + topologySpreadConstraints: [] + # -- Priority class name + priorityClassName: "" + # -- Scheduler name + schedulerName: "" + service: + # -- Service type + type: ClusterIP + ports: + # -- Service HTTP port + http: 3022 + nodePorts: + # -- Node port ("" = auto-assign) + http: "" + # -- Static cluster IP + clusterIP: "" + # -- LoadBalancer IP + loadBalancerIP: "" + # -- LoadBalancer source ranges + loadBalancerSourceRanges: [] + # -- External traffic policy + externalTrafficPolicy: Cluster + # -- Session affinity + sessionAffinity: None + # @schema + # additionalProperties: true + # @schema + # -- Session affinity config + sessionAffinityConfig: {} + # @schema + # additionalProperties: true + # @schema + # -- Service annotations (tpl-rendered) + annotations: {} + # -- Extra service ports + extraPorts: [] + ingress: + # -- Enable ingress-nginx routing for the split account console + enabled: false + # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL + hostname: "" + # -- Public console path (the generated ingress strips it) + path: /console/account + # -- Ingress path type + pathType: Prefix + # -- Ingress class name + ingressClassName: "" + # @schema + # additionalProperties: true + # @schema + # -- Ingress annotations (tpl-rendered) + annotations: {} + # -- Enable TLS for the hostname + tls: false + # -- Request a cert-manager certificate (adds kubernetes.io/tls-acme) + certManager: false + # -- Extra hosts + extraHosts: [] + # -- Extra paths for the primary host + extraPaths: [] + # -- Extra TLS entries + extraTls: [] + # -- Full custom rules (tpl-rendered; appended after the generated rules) + extraRules: [] + route: + # @schema + # type: [boolean, string] + # required: true + # @schema + # -- Create a Gateway API HTTPRoute for the account console (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) + enabled: false + # -- Route hostnames ([] = derived from server.publicUrl) hostnames: [] # -- Gateway parentRefs parentRefs: [] diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index f3f5333..418d477 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -15,7 +15,14 @@ def render(values=None, *args): command = ["helm", "template", "test", str(chart)] - if values is not None: + if isinstance(values, (str, Path)): + result = subprocess.run( + [*command, "-f", str(values), *args], + check=True, + capture_output=True, + text=True, + ) + elif values is not None: with tempfile.NamedTemporaryFile("w", suffix=".yaml") as handle: yaml.safe_dump(values, handle) handle.flush() @@ -80,8 +87,38 @@ def env_config(workload, documents): return matches[0]["data"] +def effective_env(workload, documents): + values = {} + for source in container(workload).get("envFrom", []): + reference = source.get("configMapRef") + if reference: + matches = [ + document for document in documents + if document.get("kind") == "ConfigMap" + and document["metadata"]["name"] == reference["name"] + ] + if matches: + values.update(matches[0].get("data", {})) + for entry in container(workload).get("env", []): + values[entry["name"]] = entry.get("value", "") + return values + + +def component_deployments(documents): + return { + document["metadata"]["labels"]["app.kubernetes.io/component"]: document + for document in documents + if document.get("kind") == "Deployment" + and document.get("metadata", {}).get("labels", {}).get( + "app.kubernetes.io/component" + ) + } + + def check_base(): documents = render() + deployments = component_deployments(documents) + assert not ({"auth-console", "admin-console", "account-console"} & set(deployments)) server = one(documents, "Deployment", "server") assert container(server)["args"] == ["start"] assert env_value(container(server), "WORKER_ENABLED") is None @@ -129,9 +166,41 @@ def check_base(): assert migration_mounts["logs"][0] == "/var/log/authup" +def check_split(): + documents = render(chart / "ci" / "split-values.yaml") + deployments = component_deployments(documents) + expected = { + "server": ["start", "core"], + "auth-console": ["start", "console", "auth"], + "admin-console": ["start", "console", "admin"], + "account-console": ["start", "console", "account"], + } + assert { + component: container(deployments[component])["args"] + for component in expected + } == expected + + expected_ports = { + "auth-console": 3020, + "admin-console": 3021, + "account-console": 3022, + } + for component, port in expected_ports.items(): + ports = container(deployments[component])["ports"] + assert ports == [{"name": "http", "containerPort": port, "protocol": "TCP"}] + environment = effective_env(deployments[component], documents) + assert environment["PUBLIC_URL"] == "https://auth.example.com" + assert environment["INTERNAL_URL"] == "http://test-authup-server:3000" + assert not ( + {"DB_PASSWORD", "REDIS", "SMTP", "USER_ADMIN_PASSWORD", "CLIENT_SYSTEM_SECRET"} + & set(environment) + ) + + checks = { "base": check_base, - "all": check_base, + "split": check_split, + "all": lambda: (check_base(), check_split()), } if case not in checks: From dba9e4797fc5f11b43491dca9018ad3483817e7d Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:36:15 +0200 Subject: [PATCH 03/14] feat(authup): route split consoles on one origin --- charts/authup/ci/split-values.yaml | 9 +++ charts/authup/templates/_ingress.tpl | 18 +++++ .../templates/account-console/httproute.yaml | 55 +++++++++++++ .../templates/account-console/ingress.yaml | 51 ++++++++++++ .../templates/admin-console/httproute.yaml | 64 ++++++++++++--- .../templates/admin-console/ingress.yaml | 58 ++++++++++++-- .../templates/auth-console/httproute.yaml | 55 +++++++++++++ .../templates/auth-console/ingress.yaml | 51 ++++++++++++ charts/authup/templates/server/httproute.yaml | 10 +++ charts/authup/templates/server/ingress.yaml | 10 +++ scripts/check-beta64-contract.py | 80 +++++++++++++++++++ 11 files changed, 443 insertions(+), 18 deletions(-) create mode 100644 charts/authup/templates/account-console/httproute.yaml create mode 100644 charts/authup/templates/account-console/ingress.yaml create mode 100644 charts/authup/templates/auth-console/httproute.yaml create mode 100644 charts/authup/templates/auth-console/ingress.yaml diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml index 535f9e0..904f239 100644 --- a/charts/authup/ci/split-values.yaml +++ b/charts/authup/ci/split-values.yaml @@ -21,18 +21,27 @@ server: cpu: 50m memory: 256Mi authConsole: + ingress: + enabled: true + hostname: auth.example.com resources: requests: cpu: 25m memory: 128Mi adminConsole: enabled: true + ingress: + enabled: true + hostname: auth.example.com resources: requests: cpu: 25m memory: 128Mi accountConsole: enabled: true + ingress: + enabled: true + hostname: auth.example.com resources: requests: cpu: 25m diff --git a/charts/authup/templates/_ingress.tpl b/charts/authup/templates/_ingress.tpl index 4d75ce7..c323b53 100644 --- a/charts/authup/templates/_ingress.tpl +++ b/charts/authup/templates/_ingress.tpl @@ -32,6 +32,15 @@ spec: - host: {{ $hostname | quote }} http: paths: + {{- range .firstPaths }} + - path: {{ .path }} + pathType: Exact + backend: + service: + name: {{ .serviceName }} + port: + name: http + {{- end }} - path: {{ $ing.path }} pathType: {{ $ing.pathType }} backend: @@ -110,6 +119,15 @@ spec: {{- end }} {{- end }} rules: + {{- range .exactPaths }} + - matches: + - path: + type: Exact + value: {{ . | quote }} + backendRefs: + - name: {{ $.serviceName }} + port: {{ $.servicePort }} + {{- end }} - backendRefs: - name: {{ .serviceName }} port: {{ .servicePort }} diff --git a/charts/authup/templates/account-console/httproute.yaml b/charts/authup/templates/account-console/httproute.yaml new file mode 100644 index 0000000..34dc36e --- /dev/null +++ b/charts/authup/templates/account-console/httproute.yaml @@ -0,0 +1,55 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled (include "authup.flag" (dict "value" .Values.accountConsole.route.enabled "context" $ "key" "accountConsole.route.enabled")) }} +{{- $route := .Values.accountConsole.route -}} +{{- $hostnames := $route.hostnames -}} +{{- if not $hostnames -}} +{{- $origin := include "authup.urlOrigin" (include "authup.server.publicUrl" .) -}} +{{- if $origin -}} +{{- $hostnames = list (regexReplaceAll ":[0-9]+$" (regexReplaceAll "^https?://" $origin "") "") -}} +{{- end -}} +{{- end -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $route.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $route.annotations "context" $) | nindent 4 }} + {{- end }} +spec: + {{- if $route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" $route.parentRefs "context" $) | nindent 4 }} + {{- else if .Values.server.route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" .Values.server.route.parentRefs "context" $) | nindent 4 }} + {{- end }} + {{- if $hostnames }} + hostnames: + {{- range $hostnames }} + - {{ include "authup.tplvalues.render" (dict "value" . "context" $) | quote }} + {{- end }} + {{- end }} + rules: + - matches: + {{- if $route.matches }} + {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} + {{- else }} + - path: + type: PathPrefix + value: /console/account + {{- end }} + filters: + {{- if $route.filters }} + {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} + {{- else }} + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + {{- end }} + backendRefs: + - name: {{ include "authup.accountConsole.fullname" . }} + port: {{ .Values.accountConsole.service.ports.http }} +{{- end }} diff --git a/charts/authup/templates/account-console/ingress.yaml b/charts/authup/templates/account-console/ingress.yaml new file mode 100644 index 0000000..aab760e --- /dev/null +++ b/charts/authup/templates/account-console/ingress.yaml @@ -0,0 +1,51 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled }} +{{- $ingress := .Values.accountConsole.ingress -}} +{{- $hostname := $ingress.hostname | default .Values.server.ingress.hostname -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $ingress.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- end }} + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + {{- if $ingress.ingressClassName }} + ingressClassName: {{ $ingress.ingressClassName | quote }} + {{- end }} + rules: + {{- if $hostname }} + - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + http: + paths: + - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + pathType: ImplementationSpecific + backend: + service: + name: {{ include "authup.accountConsole.fullname" . }} + port: + name: http + {{- if $ingress.extraPaths }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraPaths "context" $) | nindent 10 }} + {{- end }} + {{- end }} + {{- if $ingress.extraRules }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraRules "context" $) | nindent 4 }} + {{- end }} + {{- if or (and $hostname (or $ingress.tls $ingress.certManager)) $ingress.extraTls }} + tls: + {{- if and $hostname (or $ingress.tls $ingress.certManager) }} + - hosts: + - {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + secretName: {{ printf "%s-tls" (include "authup.accountConsole.fullname" .) | quote }} + {{- end }} + {{- if $ingress.extraTls }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraTls "context" $) | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/admin-console/httproute.yaml b/charts/authup/templates/admin-console/httproute.yaml index 4eb249d..7a8f748 100644 --- a/charts/authup/templates/admin-console/httproute.yaml +++ b/charts/authup/templates/admin-console/httproute.yaml @@ -1,11 +1,55 @@ -{{- 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" - "name" (include "authup.adminConsole.fullname" .) - "route" .Values.adminConsole.route - "serviceName" (include "authup.adminConsole.fullname" .) - "servicePort" .Values.adminConsole.service.ports.http - "publicUrl" (include "authup.adminConsole.publicUrl" .) -) }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled (include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled")) }} +{{- $route := .Values.adminConsole.route -}} +{{- $hostnames := $route.hostnames -}} +{{- if not $hostnames -}} +{{- $origin := include "authup.urlOrigin" (include "authup.server.publicUrl" .) -}} +{{- if $origin -}} +{{- $hostnames = list (regexReplaceAll ":[0-9]+$" (regexReplaceAll "^https?://" $origin "") "") -}} +{{- end -}} +{{- end -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "authup.adminConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "admin-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $route.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $route.annotations "context" $) | nindent 4 }} + {{- end }} +spec: + {{- if $route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" $route.parentRefs "context" $) | nindent 4 }} + {{- else if .Values.server.route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" .Values.server.route.parentRefs "context" $) | nindent 4 }} + {{- end }} + {{- if $hostnames }} + hostnames: + {{- range $hostnames }} + - {{ include "authup.tplvalues.render" (dict "value" . "context" $) | quote }} + {{- end }} + {{- end }} + rules: + - matches: + {{- if $route.matches }} + {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} + {{- else }} + - path: + type: PathPrefix + value: /console/admin + {{- end }} + filters: + {{- if $route.filters }} + {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} + {{- else }} + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + {{- end }} + backendRefs: + - name: {{ include "authup.adminConsole.fullname" . }} + port: {{ .Values.adminConsole.service.ports.http }} {{- end }} diff --git a/charts/authup/templates/admin-console/ingress.yaml b/charts/authup/templates/admin-console/ingress.yaml index 3133b86..918784a 100644 --- a/charts/authup/templates/admin-console/ingress.yaml +++ b/charts/authup/templates/admin-console/ingress.yaml @@ -1,9 +1,51 @@ -{{- if and .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled }} -{{- include "authup.ingress" (dict - "context" $ - "component" "admin-console" - "name" (include "authup.adminConsole.fullname" .) - "ingress" .Values.adminConsole.ingress - "serviceName" (include "authup.adminConsole.fullname" .) -) }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled }} +{{- $ingress := .Values.adminConsole.ingress -}} +{{- $hostname := $ingress.hostname | default .Values.server.ingress.hostname -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "authup.adminConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "admin-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $ingress.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- end }} + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + {{- if $ingress.ingressClassName }} + ingressClassName: {{ $ingress.ingressClassName | quote }} + {{- end }} + rules: + {{- if $hostname }} + - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + http: + paths: + - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + pathType: ImplementationSpecific + backend: + service: + name: {{ include "authup.adminConsole.fullname" . }} + port: + name: http + {{- if $ingress.extraPaths }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraPaths "context" $) | nindent 10 }} + {{- end }} + {{- end }} + {{- if $ingress.extraRules }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraRules "context" $) | nindent 4 }} + {{- end }} + {{- if or (and $hostname (or $ingress.tls $ingress.certManager)) $ingress.extraTls }} + tls: + {{- if and $hostname (or $ingress.tls $ingress.certManager) }} + - hosts: + - {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + secretName: {{ printf "%s-tls" (include "authup.adminConsole.fullname" .) | quote }} + {{- end }} + {{- if $ingress.extraTls }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraTls "context" $) | nindent 4 }} + {{- end }} + {{- end }} {{- end }} diff --git a/charts/authup/templates/auth-console/httproute.yaml b/charts/authup/templates/auth-console/httproute.yaml new file mode 100644 index 0000000..423c352 --- /dev/null +++ b/charts/authup/templates/auth-console/httproute.yaml @@ -0,0 +1,55 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled (include "authup.flag" (dict "value" .Values.authConsole.route.enabled "context" $ "key" "authConsole.route.enabled")) }} +{{- $route := .Values.authConsole.route -}} +{{- $hostnames := $route.hostnames -}} +{{- if not $hostnames -}} +{{- $origin := include "authup.urlOrigin" (include "authup.server.publicUrl" .) -}} +{{- if $origin -}} +{{- $hostnames = list (regexReplaceAll ":[0-9]+$" (regexReplaceAll "^https?://" $origin "") "") -}} +{{- end -}} +{{- end -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $route.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $route.annotations "context" $) | nindent 4 }} + {{- end }} +spec: + {{- if $route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" $route.parentRefs "context" $) | nindent 4 }} + {{- else if .Values.server.route.parentRefs }} + parentRefs: {{- include "authup.tplvalues.render" (dict "value" .Values.server.route.parentRefs "context" $) | nindent 4 }} + {{- end }} + {{- if $hostnames }} + hostnames: + {{- range $hostnames }} + - {{ include "authup.tplvalues.render" (dict "value" . "context" $) | quote }} + {{- end }} + {{- end }} + rules: + - matches: + {{- if $route.matches }} + {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} + {{- else }} + - path: + type: PathPrefix + value: /console/auth + {{- end }} + filters: + {{- if $route.filters }} + {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} + {{- else }} + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + {{- end }} + backendRefs: + - name: {{ include "authup.authConsole.fullname" . }} + port: {{ .Values.authConsole.service.ports.http }} +{{- end }} diff --git a/charts/authup/templates/auth-console/ingress.yaml b/charts/authup/templates/auth-console/ingress.yaml new file mode 100644 index 0000000..84e4460 --- /dev/null +++ b/charts/authup/templates/auth-console/ingress.yaml @@ -0,0 +1,51 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled }} +{{- $ingress := .Values.authConsole.ingress -}} +{{- $hostname := $ingress.hostname | default .Values.server.ingress.hostname -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if $ingress.annotations }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- end }} + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + {{- if $ingress.ingressClassName }} + ingressClassName: {{ $ingress.ingressClassName | quote }} + {{- end }} + rules: + {{- if $hostname }} + - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + http: + paths: + - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + pathType: ImplementationSpecific + backend: + service: + name: {{ include "authup.authConsole.fullname" . }} + port: + name: http + {{- if $ingress.extraPaths }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraPaths "context" $) | nindent 10 }} + {{- end }} + {{- end }} + {{- if $ingress.extraRules }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraRules "context" $) | nindent 4 }} + {{- end }} + {{- if or (and $hostname (or $ingress.tls $ingress.certManager)) $ingress.extraTls }} + tls: + {{- if and $hostname (or $ingress.tls $ingress.certManager) }} + - hosts: + - {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} + secretName: {{ printf "%s-tls" (include "authup.authConsole.fullname" .) | quote }} + {{- end }} + {{- if $ingress.extraTls }} + {{- include "authup.tplvalues.render" (dict "value" $ingress.extraTls "context" $) | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/server/httproute.yaml b/charts/authup/templates/server/httproute.yaml index c28478e..a519897 100644 --- a/charts/authup/templates/server/httproute.yaml +++ b/charts/authup/templates/server/httproute.yaml @@ -1,4 +1,13 @@ {{- if and .Values.server.enabled (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled")) }} +{{- $exactPaths := list -}} +{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled -}} +{{- $exactPaths = append $exactPaths "/console/admin/login/start" -}} +{{- $exactPaths = append $exactPaths "/console/admin/callback" -}} +{{- end -}} +{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled -}} +{{- $exactPaths = append $exactPaths "/console/account/login/start" -}} +{{- $exactPaths = append $exactPaths "/console/account/callback" -}} +{{- end -}} {{- include "authup.httproute" (dict "context" $ "component" "server" @@ -7,5 +16,6 @@ "serviceName" (include "authup.server.fullname" .) "servicePort" .Values.server.service.ports.http "publicUrl" (include "authup.server.publicUrl" .) + "exactPaths" $exactPaths ) }} {{- end }} diff --git a/charts/authup/templates/server/ingress.yaml b/charts/authup/templates/server/ingress.yaml index e6498ee..b65b13d 100644 --- a/charts/authup/templates/server/ingress.yaml +++ b/charts/authup/templates/server/ingress.yaml @@ -1,9 +1,19 @@ {{- if and .Values.server.enabled .Values.server.ingress.enabled }} +{{- $firstPaths := list -}} +{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled -}} +{{- $firstPaths = append $firstPaths (dict "path" "/console/admin/login/start" "serviceName" (include "authup.server.fullname" .)) -}} +{{- $firstPaths = append $firstPaths (dict "path" "/console/admin/callback" "serviceName" (include "authup.server.fullname" .)) -}} +{{- end -}} +{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled -}} +{{- $firstPaths = append $firstPaths (dict "path" "/console/account/login/start" "serviceName" (include "authup.server.fullname" .)) -}} +{{- $firstPaths = append $firstPaths (dict "path" "/console/account/callback" "serviceName" (include "authup.server.fullname" .)) -}} +{{- end -}} {{- include "authup.ingress" (dict "context" $ "component" "server" "name" (include "authup.server.fullname" .) "ingress" .Values.server.ingress "serviceName" (include "authup.server.fullname" .) + "firstPaths" $firstPaths ) }} {{- end }} diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 418d477..e4ffb4e 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -115,6 +115,17 @@ def component_deployments(documents): } +def ingress_paths(ingress): + return ingress["spec"]["rules"][0]["http"]["paths"] + + +def exact_backend(ingress, path): + matches = [item for item in ingress_paths(ingress) if item["path"] == path] + assert len(matches) == 1, f"expected one ingress path {path}, got {matches}" + assert matches[0]["pathType"] == "Exact" + return matches[0]["backend"]["service"]["name"] + + def check_base(): documents = render() deployments = component_deployments(documents) @@ -197,9 +208,78 @@ def check_split(): ) +def check_routing(): + documents = render(chart / "ci" / "split-values.yaml") + for component, name in ( + ("auth-console", "auth"), + ("admin-console", "admin"), + ("account-console", "account"), + ): + ingress = one(documents, "Ingress", component) + annotations = ingress["metadata"]["annotations"] + assert annotations["nginx.ingress.kubernetes.io/use-regex"] == "true" + assert annotations["nginx.ingress.kubernetes.io/rewrite-target"] == "/$2" + assert ingress_paths(ingress)[0]["path"] == f"/console/{name}(/|$)(.*)" + assert ingress_paths(ingress)[0]["pathType"] == "ImplementationSpecific" + + server_ingress = one(documents, "Ingress", "server") + for path in ( + "/console/admin/login/start", + "/console/admin/callback", + "/console/account/login/start", + "/console/account/callback", + ): + assert exact_backend(server_ingress, path) == "test-authup-server" + + route_values = { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + "route": {"enabled": True, "parentRefs": [{"name": "gateway"}]}, + }, + "authConsole": {"route": {"enabled": True}}, + "adminConsole": {"route": {"enabled": True}}, + "accountConsole": {"route": {"enabled": True}}, + } + routes = render(route_values) + for component, name in ( + ("auth-console", "auth"), + ("admin-console", "admin"), + ("account-console", "account"), + ): + route = one(routes, "HTTPRoute", component) + rule = route["spec"]["rules"][0] + assert rule["matches"] == [ + {"path": {"type": "PathPrefix", "value": f"/console/{name}"}} + ] + assert rule["filters"] == [ + { + "type": "URLRewrite", + "urlRewrite": { + "path": { + "type": "ReplacePrefixMatch", + "replacePrefixMatch": "/", + } + }, + } + ] + + server_route = one(routes, "HTTPRoute", "server") + rules = server_route["spec"]["rules"] + expected_paths = [ + "/console/admin/login/start", + "/console/admin/callback", + "/console/account/login/start", + "/console/account/callback", + ] + assert [rule["matches"][0]["path"]["value"] for rule in rules[:4]] == expected_paths + assert all(rule["matches"][0]["path"]["type"] == "Exact" for rule in rules[:4]) + + checks = { "base": check_base, "split": check_split, + "routing": check_routing, "all": lambda: (check_base(), check_split()), } From 356019f1214368d6edc29b115c6141c15a9ebbfb Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:40:07 +0200 Subject: [PATCH 04/14] feat(authup): add the dedicated worker workload --- charts/authup/ci/split-values.yaml | 6 + charts/authup/templates/_helpers.tpl | 4 + charts/authup/templates/_server-env.tpl | 30 +- .../authup/templates/server/deployment.yaml | 4 + .../templates/server/migration-job.yaml | 6 +- .../authup/templates/worker/deployment.yaml | 125 +++++ charts/authup/templates/worker/hpa.yaml | 33 ++ charts/authup/templates/worker/pdb.yaml | 20 + charts/authup/values.schema.json | 521 ++++++++++++++++++ charts/authup/values.yaml | 138 +++++ scripts/check-beta64-contract.py | 23 + 11 files changed, 895 insertions(+), 15 deletions(-) create mode 100644 charts/authup/templates/worker/deployment.yaml create mode 100644 charts/authup/templates/worker/hpa.yaml create mode 100644 charts/authup/templates/worker/pdb.yaml diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml index 904f239..cf79bcf 100644 --- a/charts/authup/ci/split-values.yaml +++ b/charts/authup/ci/split-values.yaml @@ -20,6 +20,12 @@ server: requests: cpu: 50m memory: 256Mi +worker: + enabled: true + resources: + requests: + cpu: 25m + memory: 128Mi authConsole: ingress: enabled: true diff --git a/charts/authup/templates/_helpers.tpl b/charts/authup/templates/_helpers.tpl index 4e088cb..17d516e 100644 --- a/charts/authup/templates/_helpers.tpl +++ b/charts/authup/templates/_helpers.tpl @@ -65,6 +65,10 @@ Usage: {{ include "authup.component.fullname" (dict "context" $ "suffix" "server {{- include "authup.component.fullname" (dict "context" . "suffix" "account-console") -}} {{- end -}} +{{- define "authup.worker.fullname" -}} +{{- include "authup.component.fullname" (dict "context" . "suffix" "worker") -}} +{{- end -}} + {{- define "authup.postgresql.fullname" -}} {{- include "authup.component.fullname" (dict "context" . "suffix" "postgresql") -}} {{- end -}} diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index ebf6a84..13f322e 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -71,34 +71,36 @@ CLIENT_SYSTEM_SECRET's key being conditional. The hook keeps exactly two: DB_PASSWORD, without which the migration cannot run, and the KEK (see below). */}} {{- define "authup.server.secretEnv" -}} -{{- $ctx := required "authup.server.secretEnv: call it as (dict \"context\" $ \"hook\" bool)" .context -}} +{{- $ctx := required "authup.server.secretEnv: context is required" .context -}} +{{- $role := .role | default "server" -}} +{{- $server := eq $role "server" -}} - name: DB_PASSWORD valueFrom: secretKeyRef: name: {{ include "authup.database.secretName" $ctx }} key: {{ include "authup.database.passwordKey" $ctx }} -{{- if and (not .hook) (include "authup.redis.enabled" $ctx) }} +{{- if and (ne $role "migration") (include "authup.redis.enabled" $ctx) }} - name: REDIS valueFrom: secretKeyRef: name: {{ include "authup.redis.secretName" $ctx }} key: {{ include "authup.redis.secretKey" $ctx }} {{- end }} -{{- if and (not .hook) (include "authup.smtp.enabled" $ctx) }} +{{- if and $server (include "authup.smtp.enabled" $ctx) }} - name: SMTP valueFrom: secretKeyRef: name: {{ include "authup.smtp.secretName" $ctx }} key: {{ include "authup.smtp.secretKey" $ctx }} {{- end }} -{{- if not .hook }} +{{- if $server }} - name: USER_ADMIN_PASSWORD valueFrom: secretKeyRef: name: {{ include "authup.auth.secretName" $ctx }} key: {{ $ctx.Values.auth.secretKeys.adminPasswordKey }} {{- end }} -{{- if and (not .hook) $ctx.Values.auth.systemClientEnabled }} +{{- if and $server $ctx.Values.auth.systemClientEnabled }} - name: CLIENT_SYSTEM_SECRET valueFrom: secretKeyRef: @@ -158,12 +160,14 @@ axis (configEnv is one define shared by the env ConfigMap and the Job's inlined env, and THEME_* must stay in its reserved-key list either way). */}} {{- define "authup.server.volumeMounts" -}} -{{- $ctx := required "authup.server.volumeMounts: call it as (dict \"context\" $ \"hook\" bool)" .context -}} +{{- $ctx := required "authup.server.volumeMounts: context is required" .context -}} +{{- $role := .role | default "server" -}} +{{- $server := eq $role "server" -}} - name: logs mountPath: /var/log/authup - name: tmp mountPath: /tmp -{{- if and (not .hook) $ctx.Values.server.provisioning.enabled (or $ctx.Values.server.provisioning.files $ctx.Values.server.provisioning.existingConfigMap $ctx.Values.server.provisioning.existingSecret) }} +{{- if and $server $ctx.Values.server.provisioning.enabled (or $ctx.Values.server.provisioning.files $ctx.Values.server.provisioning.existingConfigMap $ctx.Values.server.provisioning.existingSecret) }} - name: provisioning mountPath: /etc/authup/provisioning readOnly: true @@ -174,7 +178,7 @@ env, and THEME_* must stay in its reserved-key list either way). subPath: authup.yml readOnly: true {{- end }} -{{- if and (not .hook) (include "authup.server.themeMounted" $ctx) }} +{{- if and $server (include "authup.server.themeMounted" $ctx) }} - name: theme mountPath: {{ include "authup.server.themeMountPath" $ctx }} readOnly: true @@ -182,12 +186,14 @@ env, and THEME_* must stay in its reserved-key list either way). {{- end -}} {{- define "authup.server.volumes" -}} -{{- $ctx := required "authup.server.volumes: call it as (dict \"context\" $ \"hook\" bool)" .context -}} +{{- $ctx := required "authup.server.volumes: context is required" .context -}} +{{- $role := .role | default "server" -}} +{{- $server := eq $role "server" -}} - name: logs emptyDir: {} - name: tmp emptyDir: {} -{{- if and (not .hook) $ctx.Values.server.provisioning.enabled (or $ctx.Values.server.provisioning.files $ctx.Values.server.provisioning.existingConfigMap $ctx.Values.server.provisioning.existingSecret) }} +{{- if and $server $ctx.Values.server.provisioning.enabled (or $ctx.Values.server.provisioning.files $ctx.Values.server.provisioning.existingConfigMap $ctx.Values.server.provisioning.existingSecret) }} - name: provisioning {{- if $ctx.Values.server.provisioning.existingSecret }} secret: @@ -200,9 +206,9 @@ env, and THEME_* must stay in its reserved-key list either way). {{- if or $ctx.Values.server.configuration $ctx.Values.server.existingConfigmap }} - name: configuration configMap: - name: {{ include "authup.server.configurationConfigMapName" (dict "context" $ctx "hook" .hook) }} + name: {{ include "authup.server.configurationConfigMapName" (dict "context" $ctx "hook" (eq $role "migration")) }} {{- end }} -{{- if and (not .hook) (include "authup.server.themeMounted" $ctx) }} +{{- if and $server (include "authup.server.themeMounted" $ctx) }} - name: theme configMap: name: {{ include "authup.server.themeConfigMapName" $ctx }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index f42a479..428bd98 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -124,6 +124,10 @@ spec: name: {{ include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVarsSecret "context" $) }} {{- end }} env: {{- include "authup.server.secretEnv" (dict "context" $) | nindent 12 }} + {{- if .Values.worker.enabled }} + - name: WORKER_ENABLED + value: "false" + {{- end }} {{- if and .Values.server.migration.enabled .Release.IsUpgrade }} - name: MIGRATION_ENABLED value: "false" diff --git a/charts/authup/templates/server/migration-job.yaml b/charts/authup/templates/server/migration-job.yaml index 2d159b9..41bbd8c 100644 --- a/charts/authup/templates/server/migration-job.yaml +++ b/charts/authup/templates/server/migration-job.yaml @@ -76,7 +76,7 @@ spec: - name: {{ $key }} value: {{ $value | quote }} {{- end }} - {{- include "authup.server.secretEnv" (dict "context" $ "hook" true) | nindent 12 }} + {{- include "authup.server.secretEnv" (dict "context" $ "role" "migration") | nindent 12 }} {{- if .Values.server.extraEnvVars }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVars "context" $) | nindent 12 }} {{- end }} @@ -84,11 +84,11 @@ spec: {{- if $resources }} resources: {{- toYaml $resources | nindent 12 }} {{- end }} - volumeMounts: {{- include "authup.server.volumeMounts" (dict "context" $ "hook" true) | nindent 12 }} + volumeMounts: {{- include "authup.server.volumeMounts" (dict "context" $ "role" "migration") | nindent 12 }} {{- if .Values.server.extraVolumeMounts }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumeMounts "context" $) | nindent 12 }} {{- end }} - volumes: {{- include "authup.server.volumes" (dict "context" $ "hook" true) | nindent 8 }} + volumes: {{- include "authup.server.volumes" (dict "context" $ "role" "migration") | nindent 8 }} {{- if .Values.server.extraVolumes }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumes "context" $) | nindent 8 }} {{- end }} diff --git a/charts/authup/templates/worker/deployment.yaml b/charts/authup/templates/worker/deployment.yaml new file mode 100644 index 0000000..1f87dcf --- /dev/null +++ b/charts/authup/templates/worker/deployment.yaml @@ -0,0 +1,125 @@ +{{- if and .Values.server.enabled .Values.worker.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authup.worker.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "worker") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if not .Values.worker.autoscaling.hpa.enabled }} + replicas: {{ .Values.worker.replicaCount }} + {{- end }} + revisionHistoryLimit: {{ .Values.worker.revisionHistoryLimit }} + {{- if .Values.worker.updateStrategy }} + strategy: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.updateStrategy "context" $) | nindent 4 }} + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "worker") | nindent 6 }} + template: + metadata: + labels: {{- include "authup.labels" (dict "context" $ "component" "worker") | nindent 8 }} + {{- if .Values.worker.podLabels }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.podLabels "context" $) | nindent 8 }} + {{- end }} + annotations: + {{- if not .Values.worker.disableRestartOnChanges }} + checksum/env: {{ include "authup.server.configEnv" . | sha256sum }} + {{- if .Values.server.configuration }} + checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} + {{- end }} + {{- end }} + {{- if .Values.worker.podAnnotations }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.podAnnotations "context" $) | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "authup.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + {{- include "authup.imagePullSecrets" . | nindent 6 }} + {{- if .Values.worker.podSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.worker.podSecurityContext | nindent 8 }} + {{- end }} + {{- if .Values.worker.priorityClassName }} + priorityClassName: {{ .Values.worker.priorityClassName }} + {{- end }} + {{- if .Values.worker.schedulerName }} + schedulerName: {{ .Values.worker.schedulerName }} + {{- end }} + {{- if .Values.worker.hostAliases }} + hostAliases: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.hostAliases "context" $) | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }} + {{- if .Values.worker.affinity }} + affinity: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.affinity "context" $) | nindent 8 }} + {{- else if .Values.worker.podAntiAffinityPreset }} + affinity: {{- include "authup.podAntiAffinity" (dict "context" $ "component" "worker" "preset" .Values.worker.podAntiAffinityPreset) | nindent 8 }} + {{- end }} + {{- if .Values.worker.nodeSelector }} + nodeSelector: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.worker.tolerations }} + tolerations: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.tolerations "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.worker.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "authup.topologySpreadConstraints" (dict "context" $ "component" "worker" "constraints" .Values.worker.topologySpreadConstraints) | nindent 8 }} + {{- end }} + {{- if .Values.worker.initContainers }} + initContainers: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: worker + image: {{ include "authup.appImage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.worker.containerSecurityContext.enabled }} + securityContext: {{- include "authup.securityContext" .Values.worker.containerSecurityContext | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- toYaml .Values.diagnosticMode.command | nindent 12 }} + args: {{- toYaml .Values.diagnosticMode.args | nindent 12 }} + {{- else }} + {{- if .Values.worker.command }} + command: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.worker.args }} + args: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.args "context" $) | nindent 12 }} + {{- else }} + args: + - start + - worker + {{- end }} + {{- end }} + envFrom: + - configMapRef: + name: {{ printf "%s-env" (include "authup.server.fullname" .) }} + {{- if .Values.worker.extraEnvVarsCM }} + - configMapRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.worker.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.worker.extraEnvVarsSecret }} + - secretRef: + name: {{ include "authup.tplvalues.render" (dict "value" .Values.worker.extraEnvVarsSecret "context" $) }} + {{- end }} + env: {{- include "authup.server.secretEnv" (dict "context" $ "role" "worker") | nindent 12 }} + - name: WORKER_ENABLED + value: "true" + {{- if .Values.worker.extraEnvVars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.worker.resources }} + resources: {{- toYaml .Values.worker.resources | nindent 12 }} + {{- end }} + {{- if .Values.worker.lifecycleHooks }} + lifecycle: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: {{- include "authup.server.volumeMounts" (dict "context" $ "role" "worker") | nindent 12 }} + {{- if .Values.worker.extraVolumeMounts }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.worker.sidecars }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: {{- include "authup.server.volumes" (dict "context" $ "role" "worker") | nindent 8 }} + {{- if .Values.worker.extraVolumes }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.extraVolumes "context" $) | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/worker/hpa.yaml b/charts/authup/templates/worker/hpa.yaml new file mode 100644 index 0000000..0316e6d --- /dev/null +++ b/charts/authup/templates/worker/hpa.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.server.enabled .Values.worker.enabled .Values.worker.autoscaling.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "authup.worker.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "worker") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "authup.worker.fullname" . }} + minReplicas: {{ .Values.worker.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.worker.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.worker.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetCPU }} + {{- end }} + {{- if .Values.worker.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetMemory }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/worker/pdb.yaml b/charts/authup/templates/worker/pdb.yaml new file mode 100644 index 0000000..289628c --- /dev/null +++ b/charts/authup/templates/worker/pdb.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.server.enabled .Values.worker.enabled .Values.worker.pdb.create }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "authup.worker.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "worker") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + {{- if .Values.worker.pdb.minAvailable }} + minAvailable: {{ .Values.worker.pdb.minAvailable }} + {{- end }} + {{- if .Values.worker.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.worker.pdb.maxUnavailable }} + {{- else if not .Values.worker.pdb.minAvailable }} + maxUnavailable: 1 + {{- end }} + selector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "worker") | nindent 6 }} +{{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index f886aca..a2a3820 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -6032,6 +6032,526 @@ ], "title": "valkey", "type": "object" + }, + "worker": { + "additionalProperties": false, + "description": "#", + "properties": { + "affinity": { + "additionalProperties": true, + "description": "Affinity (overrides the anti-affinity preset when set)", + "required": [], + "title": "affinity" + }, + "args": { + "description": "Override the container args", + "items": { + "required": [] + }, + "required": [], + "title": "args", + "type": "array" + }, + "autoscaling": { + "additionalProperties": false, + "properties": { + "hpa": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": false, + "description": "Enable HPA for the worker", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "maxReplicas": { + "default": 3, + "description": "Maximum replicas", + "required": [], + "title": "maxReplicas", + "type": "integer" + }, + "minReplicas": { + "default": 1, + "description": "Minimum replicas", + "required": [], + "title": "minReplicas", + "type": "integer" + }, + "targetCPU": { + "default": 75, + "description": "Target CPU utilization percentage", + "required": [], + "title": "targetCPU", + "type": "integer" + }, + "targetMemory": { + "default": "", + "description": "Target memory utilization percentage", + "required": [], + "title": "targetMemory", + "type": "string" + } + }, + "required": [ + "enabled", + "minReplicas", + "maxReplicas", + "targetCPU", + "targetMemory" + ], + "title": "hpa", + "type": "object" + } + }, + "required": [ + "hpa" + ], + "title": "autoscaling", + "type": "object" + }, + "command": { + "description": "Override the container command", + "items": { + "required": [] + }, + "required": [], + "title": "command", + "type": "array" + }, + "containerSecurityContext": { + "additionalProperties": true, + "description": "Container security context", + "properties": { + "allowPrivilegeEscalation": { + "default": false, + "required": [], + "title": "allowPrivilegeEscalation", + "type": "boolean" + }, + "capabilities": { + "additionalProperties": false, + "properties": { + "drop": { + "items": { + "anyOf": [ + { + "required": [], + "type": "string" + } + ], + "required": [] + }, + "required": [], + "title": "drop", + "type": "array" + } + }, + "required": [ + "drop" + ], + "title": "capabilities", + "type": "object" + }, + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "default": false, + "required": [], + "title": "readOnlyRootFilesystem", + "type": "boolean" + }, + "runAsNonRoot": { + "default": false, + "required": [], + "title": "runAsNonRoot", + "type": "boolean" + }, + "runAsUser": { + "default": 0, + "required": [], + "title": "runAsUser", + "type": "integer" + }, + "seccompProfile": { + "additionalProperties": false, + "properties": { + "type": { + "default": "RuntimeDefault", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "seccompProfile", + "type": "object" + } + }, + "required": [ + "enabled", + "runAsUser", + "runAsNonRoot", + "allowPrivilegeEscalation", + "capabilities", + "seccompProfile", + "readOnlyRootFilesystem" + ], + "title": "containerSecurityContext" + }, + "disableRestartOnChanges": { + "default": false, + "description": "Disable checksum annotations that roll pods on configuration changes", + "required": [], + "title": "disableRestartOnChanges", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Deploy a dedicated background worker", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEnvVars": { + "description": "Extra environment variables for the worker container", + "items": { + "required": [] + }, + "required": [], + "title": "extraEnvVars", + "type": "array" + }, + "extraEnvVarsCM": { + "default": "", + "description": "Extra ConfigMap with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsCM", + "type": "string" + }, + "extraEnvVarsSecret": { + "default": "", + "description": "Extra Secret with environment variables (tpl-rendered name)", + "required": [], + "title": "extraEnvVarsSecret", + "type": "string" + }, + "extraVolumeMounts": { + "description": "Extra volume mounts (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumeMounts", + "type": "array" + }, + "extraVolumes": { + "description": "Extra volumes (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "extraVolumes", + "type": "array" + }, + "hostAliases": { + "description": "Pod host aliases", + "items": { + "required": [] + }, + "required": [], + "title": "hostAliases", + "type": "array" + }, + "initContainers": { + "description": "Init containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "initContainers", + "type": "array" + }, + "lifecycleHooks": { + "additionalProperties": true, + "description": "Container lifecycle hooks", + "required": [], + "title": "lifecycleHooks" + }, + "networkPolicy": { + "additionalProperties": false, + "properties": { + "allowExternalEgress": { + "default": true, + "description": "Allow all egress", + "required": [], + "title": "allowExternalEgress", + "type": "boolean" + }, + "enabled": { + "default": false, + "description": "Create an egress NetworkPolicy for the worker", + "required": [], + "title": "enabled", + "type": "boolean" + }, + "extraEgress": { + "description": "Extra egress rules", + "items": { + "required": [] + }, + "required": [], + "title": "extraEgress", + "type": "array" + } + }, + "required": [ + "enabled", + "allowExternalEgress", + "extraEgress" + ], + "title": "networkPolicy", + "type": "object" + }, + "nodeSelector": { + "additionalProperties": true, + "description": "Node selector", + "required": [], + "title": "nodeSelector" + }, + "pdb": { + "additionalProperties": false, + "properties": { + "create": { + "default": false, + "description": "Create a PodDisruptionBudget for the worker", + "required": [], + "title": "create", + "type": "boolean" + }, + "maxUnavailable": { + "default": "", + "description": "Maximum unavailable pods (defaults to 1 when both are empty)", + "required": [], + "title": "maxUnavailable", + "type": "string" + }, + "minAvailable": { + "default": "", + "description": "Minimum available pods", + "required": [], + "title": "minAvailable", + "type": "string" + } + }, + "required": [ + "create", + "minAvailable", + "maxUnavailable" + ], + "title": "pdb", + "type": "object" + }, + "podAnnotations": { + "additionalProperties": true, + "description": "Pod annotations (tpl-rendered)", + "required": [], + "title": "podAnnotations" + }, + "podAntiAffinityPreset": { + "default": "soft", + "description": "Pod anti-affinity preset: soft, hard or \"\"", + "required": [], + "title": "podAntiAffinityPreset", + "type": "string" + }, + "podLabels": { + "additionalProperties": true, + "description": "Pod labels (tpl-rendered)", + "required": [], + "title": "podLabels" + }, + "podSecurityContext": { + "additionalProperties": true, + "description": "Pod security context", + "properties": { + "enabled": { + "default": true, + "required": [], + "title": "enabled", + "type": "boolean" + }, + "fsGroup": { + "default": 1000, + "required": [], + "title": "fsGroup", + "type": "integer" + } + }, + "required": [ + "enabled", + "fsGroup" + ], + "title": "podSecurityContext" + }, + "priorityClassName": { + "default": "", + "description": "Priority class name", + "required": [], + "title": "priorityClassName", + "type": "string" + }, + "replicaCount": { + "default": 1, + "description": "Number of worker replicas (one is normally sufficient)", + "required": [], + "title": "replicaCount", + "type": "integer" + }, + "resources": { + "additionalProperties": true, + "description": "Worker container resources", + "properties": { + "limits": { + "additionalProperties": true, + "properties": { + "memory": { + "default": "1Gi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "memory" + ], + "title": "limits" + }, + "requests": { + "additionalProperties": true, + "properties": { + "cpu": { + "default": "100m", + "required": [], + "title": "cpu", + "type": "string" + }, + "memory": { + "default": "256Mi", + "required": [], + "title": "memory", + "type": "string" + } + }, + "required": [ + "cpu", + "memory" + ], + "title": "requests" + } + }, + "required": [], + "title": "resources" + }, + "revisionHistoryLimit": { + "default": 3, + "description": "Deployment revision history limit", + "required": [], + "title": "revisionHistoryLimit", + "type": "integer" + }, + "schedulerName": { + "default": "", + "description": "Scheduler name", + "required": [], + "title": "schedulerName", + "type": "string" + }, + "sidecars": { + "description": "Sidecar containers (tpl-rendered)", + "items": { + "required": [] + }, + "required": [], + "title": "sidecars", + "type": "array" + }, + "terminationGracePeriodSeconds": { + "default": 30, + "description": "Pod termination grace period", + "required": [], + "title": "terminationGracePeriodSeconds", + "type": "integer" + }, + "tolerations": { + "description": "Tolerations", + "items": { + "required": [] + }, + "required": [], + "title": "tolerations", + "type": "array" + }, + "topologySpreadConstraints": { + "description": "Topology spread constraints", + "items": { + "required": [] + }, + "required": [], + "title": "topologySpreadConstraints", + "type": "array" + }, + "updateStrategy": { + "additionalProperties": true, + "description": "Deployment update strategy", + "properties": { + "type": { + "default": "RollingUpdate", + "required": [], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "updateStrategy" + } + }, + "required": [ + "enabled", + "replicaCount", + "command", + "args", + "extraEnvVars", + "extraEnvVarsCM", + "extraEnvVarsSecret", + "extraVolumes", + "extraVolumeMounts", + "initContainers", + "sidecars", + "hostAliases", + "terminationGracePeriodSeconds", + "revisionHistoryLimit", + "disableRestartOnChanges", + "tolerations", + "podAntiAffinityPreset", + "topologySpreadConstraints", + "priorityClassName", + "schedulerName", + "pdb", + "autoscaling", + "networkPolicy" + ], + "title": "worker", + "type": "object" } }, "required": [ @@ -6054,6 +6574,7 @@ "authConsole", "adminConsole", "accountConsole", + "worker", "serviceAccount" ], "type": "object" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 2bb4bc3..287471a 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -1680,6 +1680,144 @@ accountConsole: # -- Extra egress rules extraEgress: [] +## @section Worker + +worker: + # -- Deploy a dedicated background worker + enabled: false + # -- Number of worker replicas (one is normally sufficient) + replicaCount: 1 + # -- Override the container command + command: [] + # -- Override the container args + args: [] + # -- Extra environment variables for the worker container + extraEnvVars: [] + # -- Extra ConfigMap with environment variables (tpl-rendered name) + extraEnvVarsCM: "" + # -- Extra Secret with environment variables (tpl-rendered name) + extraEnvVarsSecret: "" + # -- Extra volumes (tpl-rendered) + extraVolumes: [] + # -- Extra volume mounts (tpl-rendered) + extraVolumeMounts: [] + # -- Init containers (tpl-rendered) + initContainers: [] + # -- Sidecar containers (tpl-rendered) + sidecars: [] + # @schema + # additionalProperties: true + # @schema + # -- Container lifecycle hooks + lifecycleHooks: {} + # -- Pod host aliases + hostAliases: [] + # -- Pod termination grace period + terminationGracePeriodSeconds: 30 + # @schema + # additionalProperties: true + # @schema + # -- Deployment update strategy + updateStrategy: + type: RollingUpdate + # -- Deployment revision history limit + revisionHistoryLimit: 3 + # @schema + # additionalProperties: true + # @schema + # -- Pod annotations (tpl-rendered) + podAnnotations: {} + # @schema + # additionalProperties: true + # @schema + # -- Pod labels (tpl-rendered) + podLabels: {} + # -- Disable checksum annotations that roll pods on configuration changes + disableRestartOnChanges: false + # @schema + # additionalProperties: true + # @schema + # -- Pod security context + podSecurityContext: + enabled: true + fsGroup: 1000 + # @schema + # additionalProperties: true + # @schema + # -- Container security context + containerSecurityContext: + enabled: true + runAsUser: 0 + runAsNonRoot: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + readOnlyRootFilesystem: false + # @schema + # additionalProperties: true + # @schema + # -- Worker container resources + resources: + # @schema + # additionalProperties: true + # @schema + requests: + cpu: 100m + memory: 256Mi + # @schema + # additionalProperties: true + # @schema + limits: + memory: 1Gi + # @schema + # additionalProperties: true + # @schema + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: [] + # @schema + # additionalProperties: true + # @schema + # -- Affinity (overrides the anti-affinity preset when set) + affinity: {} + # -- Pod anti-affinity preset: soft, hard or "" + podAntiAffinityPreset: soft + # -- Topology spread constraints + topologySpreadConstraints: [] + # -- Priority class name + priorityClassName: "" + # -- Scheduler name + schedulerName: "" + pdb: + # -- Create a PodDisruptionBudget for the worker + create: false + # -- Minimum available pods + minAvailable: "" + # -- Maximum unavailable pods (defaults to 1 when both are empty) + maxUnavailable: "" + autoscaling: + hpa: + # -- Enable HPA for the worker + enabled: false + # -- Minimum replicas + minReplicas: 1 + # -- Maximum replicas + maxReplicas: 3 + # -- Target CPU utilization percentage + targetCPU: 75 + # -- Target memory utilization percentage + targetMemory: "" + networkPolicy: + # -- Create an egress NetworkPolicy for the worker + enabled: false + # -- Allow all egress + allowExternalEgress: true + # -- Extra egress rules + extraEgress: [] + ## @section Service account serviceAccount: diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index e4ffb4e..cf31ae0 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -207,6 +207,29 @@ def check_split(): & set(environment) ) + worker = one(documents, "Deployment", "worker") + worker_container = container(worker) + assert worker_container["args"] == ["start", "worker"] + assert "ports" not in worker_container + assert not ( + {"startupProbe", "livenessProbe", "readinessProbe"} + & set(worker_container) + ) + worker_env = effective_env(worker, documents) + assert worker_env["WORKER_ENABLED"] == "true" + assert effective_env(deployments["server"], documents)["WORKER_ENABLED"] == "false" + assert "DB_PASSWORD" in worker_env + assert "REDIS" in worker_env + assert "SMTP" not in worker_env + assert "USER_ADMIN_PASSWORD" not in worker_env + assert "CLIENT_SYSTEM_SECRET" not in worker_env + assert "MIGRATION_ENABLED" not in worker_env + + upgrade = render(chart / "ci" / "split-values.yaml", "--is-upgrade") + upgrade_server = one(upgrade, "Deployment", "server") + assert effective_env(upgrade_server, upgrade)["MIGRATION_ENABLED"] == "false" + assert "MIGRATION_ENABLED" not in effective_env(deployments["server"], documents) + def check_routing(): documents = render(chart / "ci" / "split-values.yaml") From 5447caf563c2513ab3219ea18ebdd3785499237f Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:43:46 +0200 Subject: [PATCH 05/14] fix(authup): cover migration and split roles with network policies --- Makefile | 2 +- charts/authup/ci/split-values.yaml | 13 +++++ .../account-console/networkpolicy.yaml | 50 +++++++++++++++++ .../admin-console/networkpolicy.yaml | 8 +-- .../templates/auth-console/networkpolicy.yaml | 50 +++++++++++++++++ .../server/migration-networkpolicy.yaml | 41 ++++++++++++++ .../templates/server/networkpolicy.yaml | 12 +++++ .../templates/worker/networkpolicy.yaml | 31 +++++++++++ scripts/check-beta64-contract.py | 53 ++++++++++++++++++- 9 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 charts/authup/templates/account-console/networkpolicy.yaml create mode 100644 charts/authup/templates/auth-console/networkpolicy.yaml create mode 100644 charts/authup/templates/server/migration-networkpolicy.yaml create mode 100644 charts/authup/templates/worker/networkpolicy.yaml diff --git a/Makefile b/Makefile index a927f38..0ca927c 100644 --- a/Makefile +++ b/Makefile @@ -33,4 +33,4 @@ template: done @echo "all ci values render" -test: lint template lint-values-coverage +test: lint template lint-values-coverage lint-beta64-contract diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml index cf79bcf..9ebe83e 100644 --- a/charts/authup/ci/split-values.yaml +++ b/charts/authup/ci/split-values.yaml @@ -15,6 +15,7 @@ server: tls: true networkPolicy: enabled: true + allowExternal: false allowExternalEgress: false resources: requests: @@ -22,11 +23,17 @@ server: memory: 256Mi worker: enabled: true + networkPolicy: + enabled: true + allowExternalEgress: false resources: requests: cpu: 25m memory: 128Mi authConsole: + networkPolicy: + enabled: true + allowExternalEgress: false ingress: enabled: true hostname: auth.example.com @@ -36,6 +43,9 @@ authConsole: memory: 128Mi adminConsole: enabled: true + networkPolicy: + enabled: true + allowExternalEgress: false ingress: enabled: true hostname: auth.example.com @@ -45,6 +55,9 @@ adminConsole: memory: 128Mi accountConsole: enabled: true + networkPolicy: + enabled: true + allowExternalEgress: false ingress: enabled: true hostname: auth.example.com diff --git a/charts/authup/templates/account-console/networkpolicy.yaml b/charts/authup/templates/account-console/networkpolicy.yaml new file mode 100644 index 0000000..ac9f6af --- /dev/null +++ b/charts/authup/templates/account-console/networkpolicy.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "authup.accountConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "account-console") | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - port: {{ .Values.accountConsole.containerPorts.http }} + protocol: TCP + {{- if not .Values.accountConsole.networkPolicy.allowExternal }} + from: + - podSelector: {} + {{- if .Values.accountConsole.networkPolicy.ingressPodMatchLabels }} + - podSelector: + matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.networkPolicy.ingressPodMatchLabels "context" $) | nindent 14 }} + {{- end }} + {{- if .Values.accountConsole.networkPolicy.ingressNSMatchLabels }} + - namespaceSelector: + matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.networkPolicy.ingressNSMatchLabels "context" $) | nindent 14 }} + {{- end }} + {{- end }} + {{- if .Values.accountConsole.networkPolicy.extraIngress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.networkPolicy.extraIngress "context" $) | nindent 4 }} + {{- end }} + egress: + {{- if .Values.accountConsole.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "server") | nindent 14 }} + {{- end }} + {{- if .Values.accountConsole.networkPolicy.extraEgress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.accountConsole.networkPolicy.extraEgress "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/admin-console/networkpolicy.yaml b/charts/authup/templates/admin-console/networkpolicy.yaml index a0ded6a..9d6966f 100644 --- a/charts/authup/templates/admin-console/networkpolicy.yaml +++ b/charts/authup/templates/admin-console/networkpolicy.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.adminConsole.enabled .Values.adminConsole.networkPolicy.enabled }} +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -14,11 +14,9 @@ spec: - Egress ingress: - ports: - - port: 3000 + - port: {{ .Values.adminConsole.containerPorts.http }} protocol: TCP {{- if not .Values.adminConsole.networkPolicy.allowExternal }} - {{- /* An empty `from:` would mean allow-all — always emit at least the - same-namespace baseline peer. */}} from: - podSelector: {} {{- if .Values.adminConsole.networkPolicy.ingressPodMatchLabels }} @@ -37,13 +35,11 @@ spec: {{- if .Values.adminConsole.networkPolicy.allowExternalEgress }} - {} {{- else }} - # DNS - ports: - port: 53 protocol: UDP - port: 53 protocol: TCP - # server-core within the release - to: - podSelector: matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "server") | nindent 14 }} diff --git a/charts/authup/templates/auth-console/networkpolicy.yaml b/charts/authup/templates/auth-console/networkpolicy.yaml new file mode 100644 index 0000000..f8c6e5d --- /dev/null +++ b/charts/authup/templates/auth-console/networkpolicy.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.server.enabled .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "authup.authConsole.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "auth-console") | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - port: {{ .Values.authConsole.containerPorts.http }} + protocol: TCP + {{- if not .Values.authConsole.networkPolicy.allowExternal }} + from: + - podSelector: {} + {{- if .Values.authConsole.networkPolicy.ingressPodMatchLabels }} + - podSelector: + matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.networkPolicy.ingressPodMatchLabels "context" $) | nindent 14 }} + {{- end }} + {{- if .Values.authConsole.networkPolicy.ingressNSMatchLabels }} + - namespaceSelector: + matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.networkPolicy.ingressNSMatchLabels "context" $) | nindent 14 }} + {{- end }} + {{- end }} + {{- if .Values.authConsole.networkPolicy.extraIngress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.networkPolicy.extraIngress "context" $) | nindent 4 }} + {{- end }} + egress: + {{- if .Values.authConsole.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "server") | nindent 14 }} + {{- end }} + {{- if .Values.authConsole.networkPolicy.extraEgress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.authConsole.networkPolicy.extraEgress "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/server/migration-networkpolicy.yaml b/charts/authup/templates/server/migration-networkpolicy.yaml new file mode 100644 index 0000000..90a2ab8 --- /dev/null +++ b/charts/authup/templates/server/migration-networkpolicy.yaml @@ -0,0 +1,41 @@ +{{- if and .Values.server.enabled .Values.server.migration.enabled .Values.server.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "authup.component.fullname" (dict "context" . "suffix" "server-migration") }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "migration") | nindent 4 }} + annotations: + {{- include "authup.annotations" (dict "context" $) | nindent 4 }} + {{- if .Values.useHelmHooks }} + helm.sh/hook: pre-upgrade + 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-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-5" + {{- end }} +spec: + podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "migration") | nindent 6 }} + policyTypes: + - Egress + egress: + {{- if .Values.server.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - podSelector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + {{- end }} + {{- if .Values.server.networkPolicy.extraEgress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.server.networkPolicy.extraEgress "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/authup/templates/server/networkpolicy.yaml b/charts/authup/templates/server/networkpolicy.yaml index 5c05752..a823c10 100644 --- a/charts/authup/templates/server/networkpolicy.yaml +++ b/charts/authup/templates/server/networkpolicy.yaml @@ -18,8 +18,20 @@ spec: protocol: TCP {{- if not .Values.server.networkPolicy.allowExternal }} from: + {{- if .Values.server.splitConsoles }} + {{- if .Values.authConsole.enabled }} + - podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "auth-console") | nindent 14 }} + {{- end }} + {{- if .Values.adminConsole.enabled }} - podSelector: matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "admin-console") | nindent 14 }} + {{- end }} + {{- if .Values.accountConsole.enabled }} + - podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "account-console") | nindent 14 }} + {{- end }} + {{- end }} {{- if .Values.server.networkPolicy.ingressPodMatchLabels }} - podSelector: matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.server.networkPolicy.ingressPodMatchLabels "context" $) | nindent 14 }} diff --git a/charts/authup/templates/worker/networkpolicy.yaml b/charts/authup/templates/worker/networkpolicy.yaml new file mode 100644 index 0000000..eca710b --- /dev/null +++ b/charts/authup/templates/worker/networkpolicy.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.server.enabled .Values.worker.enabled .Values.worker.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "authup.worker.fullname" . }} + namespace: {{ include "authup.namespace" . | quote }} + labels: {{- include "authup.labels" (dict "context" $ "component" "worker") | nindent 4 }} + annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} +spec: + podSelector: + matchLabels: {{- include "authup.matchLabels" (dict "context" $ "component" "worker") | nindent 6 }} + policyTypes: + - Egress + egress: + {{- if .Values.worker.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - podSelector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + {{- end }} + {{- if .Values.worker.networkPolicy.extraEgress }} + {{- include "authup.tplvalues.render" (dict "value" .Values.worker.networkPolicy.extraEgress "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index cf31ae0..8bb5c21 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -126,6 +126,19 @@ def exact_backend(ingress, path): return matches[0]["backend"]["service"]["name"] +def peer_components(policy, direction): + components = set() + peer_key = "from" if direction == "ingress" else "to" + for rule in policy["spec"].get(direction, []): + for peer in rule.get(peer_key, []): + component = peer.get("podSelector", {}).get("matchLabels", {}).get( + "app.kubernetes.io/component" + ) + if component: + components.add(component) + return components + + def check_base(): documents = render() deployments = component_deployments(documents) @@ -299,11 +312,49 @@ def check_routing(): assert all(rule["matches"][0]["path"]["type"] == "Exact" for rule in rules[:4]) +def check_policy(): + documents = render(chart / "ci" / "split-values.yaml") + migration = one(documents, "NetworkPolicy", "migration") + assert migration["spec"]["podSelector"]["matchLabels"][ + "app.kubernetes.io/component" + ] == "migration" + annotations = migration["metadata"]["annotations"] + assert annotations["helm.sh/hook"] == "pre-upgrade" + assert annotations["helm.sh/hook-weight"] == "-5" + assert migration["spec"]["policyTypes"] == ["Egress"] + + worker = one(documents, "NetworkPolicy", "worker") + assert worker["spec"]["policyTypes"] == ["Egress"] + + for component in ("auth-console", "admin-console", "account-console"): + policy = one(documents, "NetworkPolicy", component) + assert peer_components(policy, "egress") == {"server"} + + server = one(documents, "NetworkPolicy", "server") + assert { + "auth-console", + "admin-console", + "account-console", + } <= peer_components(server, "ingress") + + argocd_documents = render( + chart / "ci" / "split-values.yaml", + "--set", + "useHelmHooks=false", + ) + argocd = one(argocd_documents, "NetworkPolicy", "migration") + annotations = argocd["metadata"]["annotations"] + assert annotations["argocd.argoproj.io/hook"] == "PreSync" + assert annotations["argocd.argoproj.io/sync-wave"] == "-5" + assert "helm.sh/hook" not in annotations + + checks = { "base": check_base, "split": check_split, "routing": check_routing, - "all": lambda: (check_base(), check_split()), + "policy": check_policy, + "all": lambda: (check_base(), check_split(), check_routing(), check_policy()), } if case not in checks: From 92a1c46ce82061776f9714d6ed01befd0b3d90eb Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:50:42 +0200 Subject: [PATCH 06/14] feat(authup)!: finalize the beta.64 topology migration Replace the beta.63 compatibility surface with the combined and split role contract, add fail-fast migration guards, and document the required value and filesystem changes. BREAKING CHANGE: Authup beta.64 changes CLI arguments, configuration paths, console topology, and several values. See charts/authup/BREAKING.md for migration steps. --- charts/authup/BREAKING.md | 64 ++++++----- charts/authup/Chart.yaml | 22 ++-- charts/authup/README.md.gotmpl | 77 +++++++++---- charts/authup/ci/default-values.yaml | 8 +- charts/authup/ci/mysql-values.yaml | 5 - charts/authup/ci/server-only-values.yaml | 5 +- charts/authup/ci/valkey-values.yaml | 5 - charts/authup/templates/NOTES.txt | 134 ++++++++++------------- charts/authup/templates/_server-env.tpl | 16 ++- charts/authup/templates/_urls.tpl | 9 -- charts/authup/templates/validations.yaml | 21 +++- charts/authup/values.schema.json | 18 +-- charts/authup/values.yaml | 22 ++-- scripts/check-beta64-contract.py | 76 ++++++++++--- 14 files changed, 264 insertions(+), 218 deletions(-) diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index 180d316..7897e74 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -5,40 +5,38 @@ land on the middle digit. Every entry lists the value migrations required. ## Next release (unreleased) -- Component resource names are truncated on a budget derived from their suffix - (`min 52 (63 - len(suffix) - 1)`) rather than a flat `trunc 52`, so the - 63-character limit that applies to a Service name and to a Job name is - respected. Only names that were already too long to exist change: with a - release name from roughly 43 characters up, the admin-console Service was 66 - characters and the API server rejected it, so the release could not install at - all; the migration Job reached 69. Nothing to migrate, since no cluster can - hold a release in that range. Verified by rendering every release-name length - from 3 to 53 against the previous revision: the name sets differ at no length - where the old chart was installable. -- Setting BOTH `server.configuration` and `server.existingConfigmap` now fails - the render. It never worked: the existing ConfigMap is the one that gets - mounted, so the inline content was silently dropped, and that content is - typically where `db.ssl` / `socketPath` / `replication` live, i.e. how the - server pods and the pre-upgrade migration hook connect to the database. Move - the inline content into the referenced ConfigMap, or drop - `server.existingConfigmap`. -- 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 source (ConfigMap or Secret) re-mounted at `/provisioning` by - hand, because the chart's own provisioning mount stays where the chart puts it. +- The chart now requires the Authup v1.0.0-beta.64 CLI. Default server args are + `start`; split API args are `start core`; console args are + `start console auth|admin|account`; migration args are `migration run`. + Overrides containing `server/core` or `client/admin-console` must be removed. +- The default topology is one combined server. The old standalone admin + workload is no longer created merely by `adminConsole.enabled=true`. Set + `server.splitConsoles=true` to deploy separate API and console workloads. + Split mode requires `authConsole.enabled=true`; the admin and account consoles + remain independently optional. +- `server.features.accountConsole` moves to `accountConsole.enabled`. Any + non-empty old value now fails the render with the replacement key. +- `adminConsole.publicUrl`, `adminConsole.apiUrl` and + `server.trustedOriginsAppendAdminConsole` are removed. All roles share + `server.publicUrl`; split console server-side requests use the generated + in-cluster `INTERNAL_URL`. +- The configuration file is now `authup.yml`, mounted at + `/etc/authup/authup.yml`. Provisioning moves to `/etc/authup/provisioning` and + logs to `/var/log/authup`. Remove overrides for `WRITABLE_DIRECTORY_PATH`, + `/var/lib/authup`, or `authup.server.core.conf`. +- Split consoles share the Authup origin under `/console/auth`, + `/console/admin` and `/console/account`. The generated Ingress rules require + ingress-nginx because they use regex prefix stripping. Gateway API users get + portable `URLRewrite` filters. Exact admin/account login and callback paths + continue to route to the API. +- `worker.enabled=true` creates the beta.64 background worker and sets + `WORKER_ENABLED=false` on the API. The worker has no Service or HTTP probes. +- When `server.networkPolicy.enabled=true`, the chart also creates hook-scoped + migration egress policy. Restrictive split deployments get role-specific + console and worker policies; use `extraEgress` for external databases or + caches. -## 0.2.0 (unreleased) +## 0.2.0 Follows the upstream rename of the admin UI app (authup/authup#3370) and its dedicated OAuth2 client (authup/authup#3371). diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index fc9a917..9eed780 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: authup description: 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. + deploys its combined or split API, console and worker roles, with optional + built-in PostgreSQL, MySQL and Valkey instances. type: application version: 0.3.0 appVersion: "1.0.0-beta.64" @@ -31,19 +31,13 @@ annotations: - name: Source url: https://github.com/authup/helm 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 + description: Track Authup v1.0.0-beta.64 and its role-based start, migration and configuration contracts - kind: added - description: server.route.enabled / adminConsole.route.enabled accept a tpl-rendered string, so an umbrella chart can drive them from a global - - kind: fixed - description: The pre-upgrade migration Job no longer references release resources that do not exist when the hook runs; it drops the provisioning mount and every secret env migration run does not read (REDIS, SMTP, USER_ADMIN_PASSWORD, CLIENT_SYSTEM_SECRET), keeping only DB_PASSWORD and the encryption key - - kind: fixed - description: The migration Job reads authup.server.core.conf from a hook-scoped copy, so it no longer runs against a missing or one-release-stale config file - - kind: changed - description: useHelmHooks=false is documented as ArgoCD-only; under Flux or plain helm it makes the Job a plain resource whose immutable pod template fails the next upgrade + description: Optional split auth, admin and account console workloads with single-origin Ingress and Gateway API routing + - kind: added + description: Optional dedicated background worker with explicit API ownership handoff - kind: fixed - description: Component names now derive their truncation budget from the suffix, so the 63-character limit that applies to a Service name and to a Job name is respected; release names from about 43 characters up previously rendered an admin-console Service the API server rejects, and the migration Job reached 69 characters. Only names that were already too long to exist change + description: Migration and split role NetworkPolicies preserve database, cache, DNS and API connectivity under default-deny egress - kind: changed - description: Setting both server.configuration and server.existingConfigmap now fails the render instead of silently dropping the inline content, which is usually where db.ssl and socketPath live + description: Configuration, provisioning and log mounts follow the beta.64 filesystem contract diff --git a/charts/authup/README.md.gotmpl b/charts/authup/README.md.gotmpl index 12931ad..444f1ac 100644 --- a/charts/authup/README.md.gotmpl +++ b/charts/authup/README.md.gotmpl @@ -22,16 +22,15 @@ ![Version](https://img.shields.io/badge/Version-{{ template "chart.version" . }}?style=flat-square&color=informational) {{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} -{{ template "chart.description" . }} It deploys: - -- **server-core** — the Authup IdP/API service: the OAuth2/OIDC protocol - surface, the server-rendered auth pages (login, consent, registration, - password recovery) and the `/account` self-service console - (`server.features.accountConsole`). This is the identity origin. -- **client-admin-console** — the Nuxt-based admin UI, an ordinary OAuth2 relying party - (optional; disable with `adminConsole.enabled=false` for a headless IdP). -- optionally, single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in - instances on docker-official images — a convenience for dev and small +{{ template "chart.description" . }} It runs Authup v1.0.0-beta.64's role-based +CLI topology: + +- one combined API and console workload by default (`start`) +- optional split API (`start core`) plus auth, admin and account console + workloads (`start console `), enabled with `server.splitConsoles=true` +- an optional dedicated background worker (`start worker`) +- optional single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in + instances on docker-official images, a convenience for dev and small deployments, not the production database story. > This chart is pre-1.0: breaking changes land on the middle version digit and @@ -47,8 +46,8 @@ helm install authup authup/authup helm install authup oci://ghcr.io/authup/helm/authup ``` -The default install brings up server-core, the admin UI and a built-in -PostgreSQL. Retrieve the generated admin password: +The default install starts the combined server and a built-in PostgreSQL. +Retrieve the generated admin password: ```bash kubectl get secret authup -o jsonpath='{.data.admin-password}' | base64 -d @@ -58,15 +57,16 @@ kubectl get secret authup -o jsonpath='{.data.admin-password}' | base64 -d ```yaml server: + publicUrl: https://auth.example.com ingress: enabled: true hostname: auth.example.com tls: true -ui: - ingress: + migration: enabled: true - hostname: authup.example.com - tls: true + +worker: + enabled: true postgresql: enabled: false @@ -83,9 +83,44 @@ auth: existingSecret: my-authup-secret # admin-password (+ optional system-client-secret, secrets-encryption-key) ``` -`PUBLIC_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_PUBLIC_URL` and -`TRUSTED_ORIGINS` are derived from the two ingress hostnames automatically — -the UI origin is appended to the trusted origins so logins work out of the box. +`PUBLIC_URL` is derived from `server.ingress` when it is not set directly. +The combined server, split consoles and API must share this public origin. +Split Ingress resources use ingress-nginx rewrites for `/console/auth`, +`/console/admin` and `/console/account`; Gateway API HTTPRoutes express the +same topology with portable `URLRewrite` filters. + +To scale or isolate roles independently while keeping one browser origin: + +```yaml +server: + publicUrl: https://auth.example.com + splitConsoles: true + ingress: + enabled: true + hostname: auth.example.com + tls: true + +authConsole: + ingress: + enabled: true + hostname: auth.example.com + tls: true +adminConsole: + enabled: true + ingress: + enabled: true + hostname: auth.example.com + tls: true +accountConsole: + enabled: true + ingress: + enabled: true + hostname: auth.example.com + tls: true +``` + +The split auth console is required because it owns the login flow. Disable the +admin or account console independently when those surfaces are not needed. Notable operational facts (enforced or warned about by the chart): @@ -97,6 +132,10 @@ Notable operational facts (enforced or warned about by the chart): credentials and the OIDC issuer. - **`auth.secretsEncryptionKey` is write-once.** The chart never generates it; set it deliberately and back it up. +- **Configuration is mounted at `/etc/authup/authup.yml`.** Provisioning files + live under `/etc/authup/provisioning`; file logs use `/var/log/authup`. +- **The migration Job is upgrade-only.** Fresh installs let the server create + its database after built-in database resources become ready. - The long tail of Authup options is available via `server.config` (plain env name/value pairs), `server.extraEnvVars`, or a mounted `server.configuration` file. See the diff --git a/charts/authup/ci/default-values.yaml b/charts/authup/ci/default-values.yaml index f77f99a..6862e0d 100644 --- a/charts/authup/ci/default-values.yaml +++ b/charts/authup/ci/default-values.yaml @@ -1,4 +1,4 @@ -# Baseline: built-in PostgreSQL, both services. +# Baseline: combined server with built-in PostgreSQL. # The stray global key is a regression guard: helm copies a parent chart's whole # global map into every subchart before schema validation, so the schema must not # reject globals this chart does not declare. @@ -16,10 +16,6 @@ server: requests: cpu: 50m memory: 256Mi -adminConsole: - resources: - requests: - cpu: 25m - memory: 128Mi +authConsole: route: enabled: '{{ .Values.global.gatewayApiProbe }}' diff --git a/charts/authup/ci/mysql-values.yaml b/charts/authup/ci/mysql-values.yaml index 4bde7a3..d7121ca 100644 --- a/charts/authup/ci/mysql-values.yaml +++ b/charts/authup/ci/mysql-values.yaml @@ -10,8 +10,3 @@ server: requests: cpu: 50m memory: 256Mi -adminConsole: - resources: - requests: - cpu: 25m - memory: 128Mi diff --git a/charts/authup/ci/server-only-values.yaml b/charts/authup/ci/server-only-values.yaml index 60eeaba..9e5b3d6 100644 --- a/charts/authup/ci/server-only-values.yaml +++ b/charts/authup/ci/server-only-values.yaml @@ -1,9 +1,12 @@ -# Headless IdP: server-core without the admin UI. +# Combined server with the optional admin and account surfaces disabled. The +# auth pages remain available because they own the login flow. postgresql: persistence: enabled: false adminConsole: enabled: false +accountConsole: + enabled: false server: resources: requests: diff --git a/charts/authup/ci/valkey-values.yaml b/charts/authup/ci/valkey-values.yaml index b46d4e0..d154554 100644 --- a/charts/authup/ci/valkey-values.yaml +++ b/charts/authup/ci/valkey-values.yaml @@ -19,8 +19,3 @@ server: requests: cpu: 50m memory: 256Mi -adminConsole: - resources: - requests: - cpu: 25m - memory: 128Mi diff --git a/charts/authup/templates/NOTES.txt b/charts/authup/templates/NOTES.txt index 6dc0add..81adbaf 100644 --- a/charts/authup/templates/NOTES.txt +++ b/charts/authup/templates/NOTES.txt @@ -1,132 +1,110 @@ ** Authup {{ .Chart.AppVersion }} has been installed as release {{ .Release.Name }}. ** {{- $serverUrl := include "authup.server.publicUrl" . }} -{{- $uiUrl := include "authup.adminConsole.publicUrl" . }} {{- if .Values.server.enabled }} -server-core (IdP / API): -{{- if $serverUrl }} - Public URL / OIDC issuer: {{ $serverUrl }} - OIDC discovery: {{ $serverUrl }}/.well-known/openid-configuration -{{- if .Values.server.features.accountConsole }} - Account self-service: {{ $serverUrl }}/account +Runtime topology: +{{- if .Values.server.splitConsoles }} + API: start core + Auth console: start console auth +{{- if .Values.adminConsole.enabled }} + Admin console: start console admin {{- end }} -{{- else }} - In-cluster URL: http://{{ include "authup.server.fullname" . }}.{{ include "authup.namespace" . }}.svc:{{ .Values.server.service.ports.http }} - (port-forward: kubectl -n {{ include "authup.namespace" . }} port-forward svc/{{ include "authup.server.fullname" . }} 3001:{{ .Values.server.service.ports.http }}) - NOTE: no public URL is configured. Set server.publicUrl or enable - server.ingress — without a stable PUBLIC_URL, OIDC redirect URIs and - WebAuthn credentials bind to a placeholder origin, and UI logins only work - through the port-forward pair above (the UI defaults to - http://localhost:3001 as its API URL). +{{- if .Values.accountConsole.enabled }} + Account console: start console account {{- end }} -{{- if .Values.server.ingress.enabled }} - NOTE: the ingress also exposes the unauthenticated /metrics endpoint — - block it at the ingress controller or disable it via server.configuration - when it is not scraped. +{{- else }} + Combined server: start {{- end }} +{{- if .Values.worker.enabled }} + Worker: start worker {{- end }} +{{- if $serverUrl }} +Public URL / OIDC issuer: {{ $serverUrl }} +OIDC discovery: {{ $serverUrl }}/.well-known/openid-configuration +{{- if .Values.server.splitConsoles }} +Auth console: {{ $serverUrl }}{{ .Values.authConsole.ingress.path }} {{- if .Values.adminConsole.enabled }} - -client-admin-console (admin UI): -{{- if $uiUrl }} - URL: {{ $uiUrl }} +Admin console: {{ $serverUrl }}{{ .Values.adminConsole.ingress.path }} +{{- end }} +{{- if .Values.accountConsole.enabled }} +Account console: {{ $serverUrl }}{{ .Values.accountConsole.ingress.path }} +{{- end }} +{{- end }} {{- else }} - In-cluster URL: http://{{ include "authup.adminConsole.fullname" . }}.{{ include "authup.namespace" . }}.svc:{{ .Values.adminConsole.service.ports.http }} - (port-forward: kubectl -n {{ include "authup.namespace" . }} port-forward svc/{{ include "authup.adminConsole.fullname" . }} 3000:{{ .Values.adminConsole.service.ports.http }}) +In-cluster API: http://{{ include "authup.server.fullname" . }}.{{ include "authup.namespace" . }}.svc:{{ .Values.server.service.ports.http }} +Port-forward: kubectl -n {{ include "authup.namespace" . }} port-forward svc/{{ include "authup.server.fullname" . }} 3000:{{ .Values.server.service.ports.http }} + +NOTE: no public URL is configured. Set server.publicUrl or enable +server.ingress. OIDC redirect URIs and WebAuthn credentials need a stable +PUBLIC_URL. +{{- end }} +{{- if .Values.server.ingress.enabled }} + +NOTE: the server ingress also exposes the unauthenticated /metrics endpoint. +Block it at the ingress controller or disable it through server.configuration +when it is not scraped. {{- end }} {{- end }} Credentials: Username: admin {{- if .Values.auth.existingSecret }} - Password: stored in your secret "{{ include "authup.auth.secretName" . }}" (key: {{ .Values.auth.secretKeys.adminPasswordKey }}) + Password: stored in secret "{{ include "authup.auth.secretName" . }}" (key: {{ .Values.auth.secretKeys.adminPasswordKey }}) {{- else }} Password: kubectl -n {{ include "authup.namespace" . }} get secret {{ include "authup.auth.secretName" . }} -o jsonpath='{.data.{{ .Values.auth.secretKeys.adminPasswordKey }}}' | base64 -d {{- end }} {{- if .Values.auth.systemClientEnabled }} - System client secret ("system" client, for machine-to-machine consumers): + System client secret: kubectl -n {{ include "authup.namespace" . }} get secret {{ include "authup.auth.secretName" . }} -o jsonpath='{.data.{{ .Values.auth.secretKeys.systemClientSecretKey }}}' | base64 -d {{- end }} {{- if include "authup.auth.hasSecretsEncryptionKey" . }} -!! SECRETS_ENCRYPTION_KEY is configured. BACK IT UP: losing or rotating this - key makes wrapped MFA seeds and signing keys unrecoverable. -{{- end }} - -{{- if not .Values.auth.existingSecret }} -{{- if not .Values.auth.adminPassword }} - -NOTE: credentials were generated via lookup. This is NOT compatible with pure -GitOps renders (helm template / ArgoCD): every sync would apply a new random -value. In GitOps setups, set explicit auth values or auth.existingSecret. -{{- end }} +!! SECRETS_ENCRYPTION_KEY is configured. Back it up. Losing or rotating it + makes wrapped MFA seeds and signing keys unrecoverable. {{- end }} -{{- if and .Values.adminConsole.enabled (not $uiUrl) $serverUrl }} +{{- if and (not .Values.auth.existingSecret) (not .Values.auth.adminPassword) }} -WARNING: the UI has no resolvable public URL (adminConsole.publicUrl / adminConsole.ingress). Its -origin cannot be appended to TRUSTED_ORIGINS, and logins from the UI will fail -after the OAuth2 redirect. Set adminConsole.ingress.hostname or adminConsole.publicUrl. +NOTE: credentials were generated through lookup. Pure GitOps renders cannot +preserve them. Set explicit auth values or auth.existingSecret for ArgoCD and +other template-only deployment flows. {{- end }} {{- if and (gt (int .Values.server.replicaCount) 1) .Values.server.migration.enabled }} -NOTE: multi-replica server with the migration hook enabled — upgrades run -`migration run` as a pre-upgrade Job before pods roll. +NOTE: upgrades run `migration run` as a pre-upgrade Job before server pods roll. {{- else if gt (int .Values.server.replicaCount) 1 }} -TIP: with server.replicaCount > 1, consider server.migration.enabled=true so -schema migrations are serialized in a pre-upgrade Job instead of racing at -pod boot. +TIP: with server.replicaCount > 1, enable server.migration.enabled so schema +migrations are serialized before pods roll. {{- end }} {{- if and .Values.server.enabled .Values.server.migration.enabled (not .Values.useHelmHooks) }} -WARNING: useHelmHooks=false renders the migration Job as a plain resource -carrying ArgoCD PreSync annotations, which only ArgoCD reads. Under Flux or -plain helm the Job is applied like any other resource, and a Job pod template -is immutable, so the next upgrade that changes it (new image tag, new env) -fails with "spec.template: field is immutable". Use useHelmHooks=true there. -ArgoCD maps Helm hook annotations onto its own sync phases, so true works for -ArgoCD too; false only changes which annotation family drives the Job. +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. {{- end }} {{- if .Values.server.provisioning.enabled }} -NOTE: "admin-console" and "account-console" are reserved client names: authup -provisions them as built-in system clients in every realm and overwrites a -client of either name. Do not declare them in server.provisioning. +NOTE: "admin-console" and "account-console" are reserved client names. Authup +owns those built-in system clients in every realm; do not declare them in +server.provisioning. {{- end }} {{- if or .Values.postgresql.enabled .Values.mysql.enabled }} -The built-in {{ ternary "PostgreSQL" "MySQL" .Values.postgresql.enabled }} instance is a single-node convenience for dev and -small deployments. For production, consider an external database -(externalDatabase.*) or an operator (e.g. CloudNativePG). -{{- end }} - -{{- $prefixed := list }} -{{- 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 (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 }} - -NOTE: serving under a path prefix ({{ join ", " $prefixed }}). authup itself always -serves at "/", so something in the chain has to strip the prefix: a route.filters -URLRewrite (ReplacePrefixMatch "/"), an implementation-specific filter, or a proxy -in front of the gateway. Without one, every request reaches the pod with the prefix -still attached and 404s. +The built-in {{ ternary "PostgreSQL" "MySQL" .Values.postgresql.enabled }} instance is a single-node convenience for development and +small deployments. Prefer an external database or operator for production. {{- end }} {{- if $serverUrl }} -Changing PUBLIC_URL later breaks enrolled WebAuthn credentials (the rpId binds -to the origin) and changes the OIDC issuer — treat {{ $serverUrl }} as permanent. +Changing PUBLIC_URL later breaks enrolled WebAuthn credentials and changes the +OIDC issuer. Treat {{ $serverUrl }} as permanent. {{- end }} diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index 13f322e..dc0f4d5 100644 --- a/charts/authup/templates/_server-env.tpl +++ b/charts/authup/templates/_server-env.tpl @@ -43,7 +43,7 @@ CLIENT_SYSTEM_SECRET_RESET: "true" PROVISIONING_DIRECTORY_PATH: "/etc/authup/provisioning" {{- end }} LOG_DIRECTORY_PATH: "/var/log/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" "PROVISIONING_DIRECTORY_PATH" "LOG_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" }} +{{- $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" "ADMIN_CONSOLE_ENABLED" "WORKER_ENABLED" "MIGRATION_ENABLED" "MFA_ENABLED" "MFA_REQUIRED" "PROVISIONING_DIRECTORY_PATH" "LOG_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) }} @@ -55,9 +55,9 @@ LOG_DIRECTORY_PATH: "/var/log/authup" {{/* Secret-backed server-core env entries (valueFrom.secretKeyRef list). Shared by the Deployment and the migration Job. -Usage: {{ include "authup.server.secretEnv" (dict "context" $ "hook" true) }} +Usage: {{ include "authup.server.secretEnv" (dict "context" $ "role" "migration") }} -"hook" marks the pre-upgrade migration Job and drops REDIS and SMTP. Not +The migration role drops REDIS and SMTP. Not tidiness: both Secrets are ordinary release resources, and helm applies a pre-upgrade hook BEFORE the release manifest, so the upgrade that first enables valkey or SMTP would schedule a hook pod whose secretKeyRef target does not @@ -129,21 +129,19 @@ DB_PASSWORD, without which the migration cannot run, and the KEK (see below). {{/* Shared volumes / volumeMounts for the server container (logs, tmp, provisioning files, config file). -Usage: {{ include "authup.server.volumeMounts" (dict "context" $ "hook" true) }} +Usage: {{ include "authup.server.volumeMounts" (dict "context" $ "role" "migration") }} The `required` on .context is load-bearing: helm renders with missingkey=zero, so a call site that passed a bare `.` would leave every guard below reading false and emit logs+tmp only, silently dropping the config file. Failing the render is the chart's posture everywhere else. -"hook" marks the pre-upgrade migration Job. It drops the provisioning mount, +The migration role drops the provisioning mount, whose ConfigMap/Secret is an ordinary release resource that helm applies AFTER the hook: the upgrade that first sets server.provisioning.files would leave the hook pod in ContainerCreating on a "configmap not found" until it times out, and `migration run` never reads those files anyway (ProvisionerModule is registered -by the start command only). The writable directory stays for BOTH: under the -image's NODE_ENV=production the logger opens /http.log and -/error.log before the migration touches the database, and an -uncreatable path is a hard ENOENT failure. +by the start command only). Every server role keeps the log mount because the +logger opens files before the role-specific modules start. The config file stays for both as well, and mounting it is not optional: `migration run` loads authup.yml unconditionally, and the db keys diff --git a/charts/authup/templates/_urls.tpl b/charts/authup/templates/_urls.tpl index 0de8f3b..05e7dbc 100644 --- a/charts/authup/templates/_urls.tpl +++ b/charts/authup/templates/_urls.tpl @@ -33,15 +33,6 @@ scheme-less result can never reach an env var or origin derivation. {{- end -}} {{- end -}} -{{/* Compatibility aliases for templates migrated in later slices. */}} -{{- define "authup.adminConsole.publicUrl" -}} -{{- include "authup.server.publicUrl" . -}} -{{- end -}} - -{{- define "authup.adminConsole.apiUrl" -}} -{{- include "authup.server.publicUrl" . -}} -{{- end -}} - {{/* Extract the origin (scheme://host[:port]) from a URL. */}} diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index 01484b5..7c1c67b 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -45,6 +45,17 @@ database is configured while the mounted file is the operator's. {{- fail "authup: server.configuration and server.existingConfigmap are mutually exclusive. The existing ConfigMap is the one that gets mounted, so the inline content would be silently dropped: move it into that ConfigMap, or drop server.existingConfigmap." }} {{- end }} +{{- if not (eq (toString .Values.server.features.accountConsole) "") }} +{{- fail "authup: server.features.accountConsole moved to accountConsole.enabled in the beta.64 topology." }} +{{- end }} + +{{- if and .Values.server.splitConsoles (not .Values.server.enabled) }} +{{- fail "authup: server.splitConsoles requires server.enabled=true." }} +{{- end }} +{{- if and .Values.server.splitConsoles (not .Values.authConsole.enabled) }} +{{- fail "authup: server.splitConsoles requires authConsole.enabled=true because the auth console owns the login flow." }} +{{- end }} + {{/* An external database password must be deliberate — never silently generated for a database the chart does not own. */}} {{- if and (not .Values.postgresql.enabled) (not .Values.mysql.enabled) .Values.externalDatabase.host (not .Values.externalDatabase.password) (not .Values.externalDatabase.existingSecret) }} {{- fail "authup: externalDatabase.host is set but neither externalDatabase.password nor externalDatabase.existingSecret is — the chart will not invent a password for a database it does not manage." }} @@ -53,9 +64,15 @@ database is configured while the mounted file is the operator's. {{- if and .Values.server.ingress.enabled (not .Values.server.ingress.hostname) (not .Values.server.ingress.extraHosts) (not .Values.server.ingress.extraRules) }} {{- fail "authup: server.ingress.enabled requires server.ingress.hostname (or extraHosts / extraRules)." }} {{- end }} -{{- if and .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.adminConsole.ingress.extraHosts) (not .Values.adminConsole.ingress.extraRules) }} +{{- if and .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled (not .Values.authConsole.ingress.hostname) (not .Values.authConsole.ingress.extraHosts) (not .Values.authConsole.ingress.extraRules) }} +{{- fail "authup: authConsole.ingress.enabled requires authConsole.ingress.hostname (or extraHosts / extraRules)." }} +{{- end }} +{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.adminConsole.ingress.extraHosts) (not .Values.adminConsole.ingress.extraRules) }} {{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname (or extraHosts / extraRules)." }} {{- end }} +{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled (not .Values.accountConsole.ingress.hostname) (not .Values.accountConsole.ingress.extraHosts) (not .Values.accountConsole.ingress.extraRules) }} +{{- fail "authup: accountConsole.ingress.enabled requires accountConsole.ingress.hostname (or extraHosts / extraRules)." }} +{{- end }} {{/* The route flags are validated here unconditionally. The render gates spell @@ -65,7 +82,9 @@ 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.authConsole.route.enabled "context" $ "key" "authConsole.route.enabled") }} {{- $_ = include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled") }} +{{- $_ = include "authup.flag" (dict "value" .Values.accountConsole.route.enabled "context" $ "key" "accountConsole.route.enabled") }} {{/* A Gateway API HTTPRoute rule without `matches` defaults to PathPrefix "/", i.e. it diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index a2a3820..944aa95 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -4317,14 +4317,14 @@ }, "configuration": { "default": "", - "description": "Content of an authup.server.core.conf mounted into the working directory\nfor file-only options (middleware objects, per-field SMTP, CORS allowlist).\nEnvironment variables always win over file values.", + "description": "Content of authup.yml mounted at /etc/authup/authup.yml for file-only\noptions (middleware objects, per-field SMTP, CORS allowlist). Environment\nvariables always win over file values.", "required": [], "title": "configuration", "type": "string" }, "containerSecurityContext": { "additionalProperties": true, - "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.", + "description": "Container security context. The upstream image runs as root and needs\nwritable log and npm cache paths; the chart mounts emptyDirs at\n/var/log/authup and /tmp.", "properties": { "allowPrivilegeEscalation": { "default": false, @@ -4442,7 +4442,7 @@ }, "existingConfigmap": { "default": "", - "description": "Existing ConfigMap holding authup.server.core.conf (tpl-rendered)", + "description": "Existing ConfigMap holding authup.yml (tpl-rendered)", "required": [], "title": "existingConfigmap", "type": "string" @@ -4850,7 +4850,7 @@ }, "enabled": { "default": false, - "description": "Run `server/core migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls migrate at boot regardless.", + "description": "Run `migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls migrate at boot regardless.", "required": [], "title": "enabled", "type": "boolean" @@ -5077,7 +5077,7 @@ }, "publicUrl": { "default": "", - "description": "Public URL of server-core (PUBLIC_URL) — the OIDC issuer origin.\n\"\" = derived from server.ingress when enabled. Changing it later breaks\nenrolled WebAuthn credentials and the OIDC issuer.", + "description": "Public URL of Authup (PUBLIC_URL), shared by the API and consoles and\nused as the OIDC issuer origin.\n\"\" = derived from server.ingress when enabled. Changing it later breaks\nenrolled WebAuthn credentials and the OIDC issuer.", "required": [], "title": "publicUrl", "type": "string" @@ -5593,13 +5593,6 @@ "title": "trustedOrigins", "type": "array" }, - "trustedOriginsAppendAdminConsole": { - "default": true, - "description": "Automatically append the client-admin-console UI origin to TRUSTED_ORIGINS (removes the\nmost common dead-login misconfiguration)", - "required": [], - "title": "trustedOriginsAppendAdminConsole", - "type": "boolean" - }, "updateStrategy": { "additionalProperties": true, "description": "Deployment update strategy", @@ -5623,7 +5616,6 @@ "replicaCount", "publicUrl", "trustedOrigins", - "trustedOriginsAppendAdminConsole", "trustProxy", "features", "mfa", diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 287471a..94375c3 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -407,7 +407,8 @@ server: splitConsoles: false # -- Number of server-core replicas (values > 1 REQUIRE a configured cache) replicaCount: 1 - # -- Public URL of server-core (PUBLIC_URL) — the OIDC issuer origin. + # -- Public URL of Authup (PUBLIC_URL), shared by the API and consoles and + # used as the OIDC issuer origin. # "" = derived from server.ingress when enabled. Changing it later breaks # enrolled WebAuthn credentials and the OIDC issuer. publicUrl: "" @@ -418,9 +419,6 @@ server: # (https://*.example.com); "**" in a host is rejected by authup at boot. # List or comma-separated string; tpl-rendered. trustedOrigins: [] - # -- Automatically append the client-admin-console UI origin to TRUSTED_ORIGINS (removes the - # most common dead-login misconfiguration) - trustedOriginsAppendAdminConsole: true # -- TRUST_PROXY setting. The chart defaults to one trusted hop (the ingress), # not authup's spoofable trust-everything default trustProxy: "1" @@ -451,11 +449,11 @@ server: # with your own build (pair them with extraVolumes; the substituted package # owns the login flow, so use server.theme for branding instead) config: {} - # -- 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. + # -- Content of authup.yml mounted at /etc/authup/authup.yml for file-only + # options (middleware objects, per-field SMTP, CORS allowlist). Environment + # variables always win over file values. configuration: "" - # -- Existing ConfigMap holding authup.server.core.conf (tpl-rendered) + # -- Existing ConfigMap holding authup.yml (tpl-rendered) existingConfigmap: "" provisioning: # -- Mount provisioning files consumed at boot (fail-closed: an invalid file aborts startup) @@ -526,7 +524,7 @@ server: # 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 + # -- Run `migration run` as a pre-upgrade hook Job. Recommended # for multi-replica deployments (serializes DDL before pods roll). Fresh # installs migrate at boot regardless. enabled: false @@ -601,9 +599,9 @@ server: # @schema # additionalProperties: true # @schema - # -- 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. + # -- Container security context. The upstream image runs as root and needs + # writable log and npm cache paths; the chart mounts emptyDirs at + # /var/log/authup and /tmp. containerSecurityContext: enabled: true runAsUser: 0 diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 8bb5c21..4bad669 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -13,12 +13,11 @@ case = sys.argv[2] if len(sys.argv) > 2 else "all" -def render(values=None, *args): +def render_result(values=None, *args): command = ["helm", "template", "test", str(chart)] if isinstance(values, (str, Path)): - result = subprocess.run( + return subprocess.run( [*command, "-f", str(values), *args], - check=True, capture_output=True, text=True, ) @@ -26,22 +25,30 @@ def render(values=None, *args): with tempfile.NamedTemporaryFile("w", suffix=".yaml") as handle: yaml.safe_dump(values, handle) handle.flush() - result = subprocess.run( + return subprocess.run( [*command, "-f", handle.name, *args], - check=True, capture_output=True, text=True, ) - else: - result = subprocess.run( - [*command, *args], - check=True, - capture_output=True, - text=True, - ) + return subprocess.run( + [*command, *args], + capture_output=True, + text=True, + ) + + +def render(values=None, *args): + result = render_result(values, *args) + result.check_returncode() return [document for document in yaml.safe_load_all(result.stdout) if document] +def render_fails(values, message): + result = render_result(values) + assert result.returncode != 0, f"expected render failure containing {message!r}" + assert message in result.stderr, result.stderr + + def one(documents, kind, component, suffix=None): matches = [ document for document in documents @@ -243,6 +250,17 @@ def check_split(): assert effective_env(upgrade_server, upgrade)["MIGRATION_ENABLED"] == "false" assert "MIGRATION_ENABLED" not in effective_env(deployments["server"], documents) + rendered = render_result(chart / "ci" / "split-values.yaml").stdout + for retired in ( + "server/core", + "client/admin-console", + "NUXT_", + "WRITABLE_DIRECTORY_PATH", + "/var/lib/authup", + "authup.server.core.conf", + ): + assert retired not in rendered, f"retired runtime contract remains: {retired}" + def check_routing(): documents = render(chart / "ci" / "split-values.yaml") @@ -349,12 +367,44 @@ def check_policy(): assert "helm.sh/hook" not in annotations +def check_validations(): + render_fails( + {"server": {"features": {"accountConsole": False}}}, + "server.features.accountConsole moved to accountConsole.enabled", + ) + render_fails( + {"server": {"enabled": False, "splitConsoles": True}}, + "server.splitConsoles requires server.enabled=true", + ) + render_fails( + {"server": {"splitConsoles": True}, "authConsole": {"enabled": False}}, + "server.splitConsoles requires authConsole.enabled=true", + ) + for component in ("authConsole", "adminConsole", "accountConsole"): + render_fails( + {component: {"enabled": False, "route": {"enabled": "invalid"}}}, + f"{component}.route.enabled must be true or false", + ) + for name in ("ADMIN_CONSOLE_ENABLED", "WORKER_ENABLED", "MIGRATION_ENABLED"): + render_fails( + {"server": {"config": {name: "false"}}}, + f"server.config.{name} collides with a first-class chart value", + ) + + checks = { "base": check_base, "split": check_split, "routing": check_routing, "policy": check_policy, - "all": lambda: (check_base(), check_split(), check_routing(), check_policy()), + "validations": check_validations, + "all": lambda: ( + check_base(), + check_split(), + check_routing(), + check_policy(), + check_validations(), + ), } if case not in checks: From 8ed762bdea0e2fcb0236a6873a5216e09f4ef354 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 16:56:46 +0200 Subject: [PATCH 07/14] docs(authup): record the beta.64 deployment contract --- .agents/architecture.md | 325 +++++--------- .agents/references/authup.md | 182 ++++---- .agents/structure.md | 112 +++-- .agents/testing.md | 232 +++------- AGENTS.md | 16 +- DESIGN.md | 747 ++++++++++--------------------- charts/authup/README.md | 353 +++++++++++++-- charts/authup/values.schema.json | 4 +- charts/authup/values.yaml | 4 +- 9 files changed, 890 insertions(+), 1085 deletions(-) diff --git a/.agents/architecture.md b/.agents/architecture.md index da3b7c6..36d4ba6 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -1,214 +1,131 @@ # Architecture -`DESIGN.md` at the repo root holds the full design rationale with evidence. -This file lists the operational invariants an agent must not break when -editing templates or values. +`DESIGN.md` is the authoritative rationale. This file is the compact list of +operational invariants that template changes must preserve. -## Load-bearing rules +## Authup beta.64 runtime contract -1. **containerPort is always 3000, for both services.** The authup image - entrypoint force-exports `PORT=3000` / `NUXT_PORT=3000`; a chart-set `PORT` - env is dead. Never surface a containerPort value. -2. **Strict booleans render quoted.** authup's `readBoolStrict` env reader - (`EVENT_LOG_*`, `MFA_*`, `LOGIN_ATTEMPT_THROTTLE_ENABLED`) crashes the boot - on unparsable values. Every boolean env in `authup.server.configEnv` goes - through `toString | quote`. -3. **The cache env var is `REDIS`** (a full connection URL), not `REDIS_URL` - (a documentation ghost that never existed in authup source). The URL embeds - the password, so it always lives in a Secret and reaches the pod via - `valueFrom.secretKeyRef`. -4. **A database is mandatory.** The published image bakes - `NODE_ENV=production`, which forbids sqlite. `validations.yaml` hard-fails - when neither a built-in engine nor `externalDatabase.host` is configured. -5. **replicas > 1 requires a cache.** Without redis, authup falls back to a - per-process memory cache: authorization codes, token revocations and MFA - challenges break across replicas (functional breakage, not just - performance). `validations.yaml` enforces this. -6. **`SECRETS_ENCRYPTION_KEY` is write-once and never generated.** Losing or - rotating it bricks wrapped MFA seeds and signing keys. From an existing - secret it requires the explicit `auth.secretsEncryptionKeyEnabled` opt-in, - and the secretKeyRef is never `optional:` (a silently missing KEK would - fail open into plaintext-at-rest). -7. **URL values must carry a scheme, asserted twice.** `validations.yaml` - checks literal values; the `_urls.tpl` helpers re-assert AFTER tpl - rendering (`authup.assertUrlScheme`), because a template-valued URL only - materializes there. `authup.urlOrigin` returns "" unless both scheme and - host parse, so a broken origin can never reach `TRUSTED_ORIGINS`. -8. **Selectors are immutable and minimal.** `authup.matchLabels` emits only - name + instance + component. `commonLabels` / `podLabels` must never leak - into a selector. `app.kubernetes.io/component` separates the two services' - Services within one release. -9. **Component fullnames truncate the base BEFORE suffixing, on a budget - derived from the suffix.** `authup.component.fullname` - (`dict "context" $ "suffix" "server"`) is the single implementation; every - component name and the migration Job go through it. Truncating first is what - keeps names DISTINCT (a 63-char fullname would otherwise collapse every - component onto one name); deriving the budget is what keeps them LEGAL. +1. **One image, explicit roles.** The supported default args are `start` for the + combined server, `start core` for the split API, `start console auth|admin|account` + for split consoles, `start worker` for the worker, and `migration run` for the + upgrade Job. Do not restore `server/core`, `client/admin-console`, or a second + image. +2. **Combined is the default.** `server.splitConsoles=false` creates one server + Deployment. Split mode changes the server role to core and creates console + Deployments. The auth console is required in split mode because it owns login; + admin and account remain independently optional. +3. **Role ports come from Authup.** Core listens on 3000. Split auth, admin and + account consoles listen on 3020, 3021 and 3022. The worker has no listener, + Service, or HTTP probe. +4. **Worker ownership is explicit.** `worker.enabled=true` sets + `WORKER_ENABLED=true` on the worker and `WORKER_ENABLED=false` on the server. + The worker gets database and Redis credentials, but not SMTP, bootstrap + identity secrets, migrations, or console secrets. +5. **The filesystem contract is fixed.** Configuration is `authup.yml` at + `/etc/authup/authup.yml`, provisioning is `/etc/authup/provisioning`, and + logs are `/var/log/authup`. There is no chart-managed writable root and no + `WRITABLE_DIRECTORY_PATH`. - The ceiling is 63, not the 253 a ConfigMap allows, wherever a name becomes a - DNS-1035 label (Service) or a label value (a Job name is copied into the - `job-name` pod labels). The old flat `trunc 52` ignored that: `-admin-console` - rendered a 66-char Service, so any release name from ~43 characters up could - not install at all, and appending `-migration` to the `-server` name reached - 69. Both are now `min 52 (63 - len(suffix) - 1)`. +## Configuration and state - `min 52` is the load-bearing half. The derived budget is WIDER than 52 for - short suffixes, and widening RENAMES resources on releases whose fullname - lands between 53 and 55 characters. A renamed Secret carrying - `helm.sh/resource-policy: keep` orphans the old one and generates a new admin - password: a silent credential rotation on upgrade. **The budget may only ever - tighten**, which by construction touches only names too long to exist. Assert - that when changing it (see testing.md), do not assume it. -10. **The migration Job shares the deployment's env by construction, minus - what a hook cannot see.** `authup.server.configEnv` (map), - `authup.server.secretEnv` (list) and the two volume helpers are the single - sources consumed by both `server/deployment.yaml` and - `server/migration-job.yaml`; the Job INLINES the config map (a pre-upgrade - hook would otherwise run against the previous release's ConfigMap). The - Job is pre-upgrade ONLY (never pre-install: hooks run before backing - services exist; authup migrates at boot on fresh installs). With - `useHelmHooks=false` it renders ArgoCD `PreSync` hook annotations instead, - which is an ArgoCD-only mode: see rule 19. +6. **Strict booleans render quoted.** Authup's strict env reader fails boot on + malformed values. First-class boolean env values go through + `toString | quote`. +7. **The cache env var is `REDIS`.** It is a full connection URL, not + `REDIS_URL`. Because it embeds credentials, it comes from a Secret through + `secretKeyRef`. +8. **A database is mandatory.** The production image cannot use SQLite. + `validations.yaml` fails unless built-in PostgreSQL, built-in MySQL, or + `externalDatabase.host` is configured. +9. **Multiple API replicas require shared cache.** Without Redis, Authup falls + back to per-process state for authorization codes, revocations and MFA + challenges. Replica counts above one and HPA therefore fail without cache. +10. **`SECRETS_ENCRYPTION_KEY` is write-once and never generated.** Losing or + rotating it makes wrapped rows unreadable. Existing-secret use requires an + explicit opt-in and the reference is never optional. +11. **No config-schema mirror.** First-class values cover load-bearing options; + `server.config`, extra env carriers and `server.configuration` cover the + long tail. `server.config` keys that collide with a first-class variable + fail the render. The small theme manifest is the only deliberate mirrored + file format. +12. **Secrets never render as pod env literals.** Inline secret values are + stored in chart-managed Secrets and referenced with `secretKeyRef`. An + external database password is never invented. +13. **Generated credentials use lookup-or-generate.** The chart-managed auth + Secret is lookup-stable under Helm and kept with a resource policy. Pure + template GitOps cannot preserve generated values, so those users must set + explicit values or existing Secrets. - Helm applies a pre-upgrade hook BEFORE the release manifest, so every - NON-HOOK resource the Job references must already exist from the PREVIOUS - release. A hook resource at a lower weight is the one exception: it is - created earlier in the same hook phase, which is exactly what the config - copy below relies on. Four - helpers take a `hook` flag (`secretEnv`, the two volume helpers and - `configurationConfigMapName`; `configEnv` does not, it is inlined instead) - and drop what `migration run` does not read. That flag is the ONE mechanism - for this: the theme volume used to be a pair of deployment-only defines - carved out for the same reason, and two conventions in one `volumeMounts:` - block is how the next mount ends up on the wrong side. `themeEnv` stays - separate because it splits along a different axis. Dropped: - `REDIS`, `SMTP` (their Secrets are release resources, and the migration - builds no cache or mail module) and the provisioning mount (`ProvisionerModule` - is registered by the start command only). What stays, stays for a reason: - the writable directory, because under the image's `NODE_ENV=production` the - logger opens `/http.log` before the first query and an uncreatable - path is a hard ENOENT; and the config file, because `migration run` loads - `authup.server.core.conf` unconditionally and its file-only db keys (`ssl`, - `socketPath`, `replication`, `extensions`) decide how the migration connects. - The Job reads that file from a hook-scoped COPY - (`server/configmap-migration-configuration.yaml`, weight -5) for the same - reason it inlines the env: the release ConfigMap is either absent or one - release stale when the hook runs. `USER_ADMIN_PASSWORD` and - `CLIENT_SYSTEM_SECRET` go the same way: no identity or provisioning module - on the migration path, and the auth Secret they read is itself a release - resource. `SECRETS_ENCRYPTION_KEY` deliberately does NOT, even though its - key is conditional too and the migration does not read it today: rule 6's - fail-closed posture outranks the one-off break, so a write-once KEK gets its - own upgrade. +## URLs and routing - What the flag cannot reach, i.e. the residuals to keep in mind when adding - anything to the Job: `DB_PASSWORD` (the Secret behind it changes on an engine - switch, on adopting a built-in engine after `externalDatabase`, and on a - first inline `externalDatabase.password`, since `secret-db.yaml` is a release - resource too); the `serviceAccountName`, whose ServiceAccount renders only - under `serviceAccount.create`, so flipping that on fails pod ADMISSION with - no container status to read; and the `extraEnvVarsCM` / `extraEnvVarsSecret` - / `extraVolumes` passthroughs, whose targets are operator-owned unless the - operator ships them through `extraDeploy`, which renders them into the - release manifest and therefore after the hook. -11. **Checksum annotations roll pods on config or secret changes.** The server - deployment checksums the env map plus every chart-managed secret it - consumes (auth, external-db, redis, smtp, provisioning, configuration), - each guarded by the same condition the secret renders under. - `disableRestartOnChanges` opts out. -12. **Secrets never render as pod env literals.** Inline values land in - chart-managed Secrets referenced via `secretKeyRef`; the external-db - password is never generated (render-time fail instead: the chart does not - invent credentials for a database it does not manage). -13. **Generated credentials use lookup-or-generate** (`authup.secret.rawValue`) - with `helm.sh/resource-policy: keep`. This is incompatible with pure - GitOps renders (lookup is inert under `helm template` / ArgoCD): NOTES and - the README warn; GitOps users set explicit values or `existingSecret`. - Values that feed BOTH a password key and a composed connection string - (valkey) are resolved once per render inside a single Secret template so - the two keys cannot diverge on fresh installs. -14. **No config-file re-templating.** authup is env-configured; the chart - renders env vars plus escape hatches (`server.config`, - `extraEnvVars`/`extraEnvVarsCM`/`extraEnvVarsSecret`, - `server.configuration` file mount). Never mirror authup's config schema in - templates (Authelia's 714-line configMap treadmill is the cautionary tale). - `server.config` keys colliding with first-class env names fail the render. - The ONE mirrored schema is the theme manifest (`server.theme.title` / - `logo` / `tokens` / ... compose `theme.json`), and it earns the exception - on three counts: the file is a fixed 8-key document rather than a growing - config surface, authup fails the BOOT on an unknown key or a malformed - token so a typo has no cheaper detector, and the alternative is a JSON - blob inside a YAML string with no schema at all. It stays worth it only - while the manifest stays small: `files` remains the escape hatch, and a - hand-written `theme.json` there is still supported (the two are mutually - exclusive by validation). -15. **URL derivation is the chart's core UX.** `PUBLIC_URL`, - `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_PUBLIC_URL` derive from the two - ingress blocks; the UI origin is auto-appended to `TRUSTED_ORIGINS` - (`server.trustedOriginsAppendAdminConsole`). A missing trusted origin is the #1 - dead-login misconfiguration. The chart never sets - `NUXT_PUBLIC_COOKIE_DOMAIN`: sharing a cookie domain between client-admin-console - and the hosted auth pages is unsupported by authup. -16. **Every list/map passthrough is tpl-rendered** via - `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 - uninstallable as a dependency of any umbrella that sets a global this - chart does not declare. `values.yaml` carries the - `# @schema additionalProperties: true` opt-out and `ci/default-values.yaml` - a stray global key as the regression guard. The chart reads only - `imageRegistry` / `imagePullSecrets` / `defaultStorageClass` and ignores - the rest. -18. **An HTTPRoute rule with no `matches` is a catch-all.** The Gateway API - defaults an empty `matches` to PathPrefix `/`, and route hostnames come - from the public URL's ORIGIN (the path is dropped), so a sub-path - deployment would silently take over the whole shared hostname. - `validations.yaml` fails that combination; `route.matches` / `route.filters` - are the raw passthroughs that express it (authup always serves at `/`, so - the prefix must be matched AND rewritten away). -19. **`useHelmHooks=false` is an ArgoCD-only mode.** ArgoCD renders with - `helm template` and never executes Helm hooks, so it needs its own - `argocd.argoproj.io/hook` annotations. Flux is the opposite: helm-controller - runs a real `helm upgrade` and honours Helm hooks natively. Turning them off - there applies the migration Job as an ordinary release resource, and - `Job.spec.template` is immutable, so the next upgrade that touches the pod - template (image tag, `appVersion` label, a new env) fails to patch it. A - content-hashed Job name would make that apply-able but not correct: helm - orders a plain Job AFTER the Deployment and does not wait for it, which is - the ordering the Job exists to provide. So the value stays doc-scoped to - ArgoCD and NOTES warns when it is set. ArgoCD also maps Helm hooks onto its - own sync phases, so `true` works there as well; the flag only chooses which - annotation family drives the Job. +14. **Every browser-facing role shares `server.publicUrl`.** It is either set + explicitly or derived from server Ingress. Literal and template-rendered + URLs are both checked for an HTTP scheme. Split consoles receive the same + `PUBLIC_URL` and use an in-cluster `INTERNAL_URL` for server-side API calls. +15. **Split consoles preserve one origin.** They are exposed under + `/console/auth`, `/console/admin` and `/console/account`. Generated Ingress + resources use ingress-nginx regex rewrites. Gateway API routes use + `URLRewrite` with `ReplacePrefixMatch`. Exact admin/account login and + callback paths must remain on the API before broader console prefixes. +16. **HTTPRoute flags are strict.** `route.enabled` accepts a boolean or a + template-rendered boolean string. `authup.flag` validates every role even + when that role is disabled, because a non-empty string `"false"` is truthy + to Go templates. +17. **An empty HTTPRoute match is a catch-all.** A server public URL carrying a + path requires explicit match and rewrite rules. The validation prevents a + sub-path deployment from taking over the full hostname. -## Values conventions +## Workload and hook safety -- bitnami-shaped keys: `fullnameOverride`, `existingSecret` + `secretKeys` - key-mapping, `extraEnvVars`/`extraEnvVarsCM`/`extraEnvVarsSecret`, - `extraVolumes`/`extraVolumeMounts`, `initContainers`/`sidecars`, - `extraDeploy`, `commonLabels`/`commonAnnotations`, `diagnosticMode`, - `useHelmHooks`. -- `values.yaml` is the single documentation source: `# --` comments feed - helm-docs, `# @schema` blocks feed helm-schema. Every free-form or - extensible map carries `# @schema additionalProperties: true` - the - generated schema is strict (`additionalProperties: false`) everywhere else, - which is what turns value typos into install-time errors. When adding a new - map value that users extend (annotations, selectors, resources-like), add - the annotation or the schema will silently forbid its use. -- Cross-field rules that the JSON schema cannot express live in - `templates/validations.yaml` (render-nothing fail-fast guards). When a value - moves, add a tripwire there that names the new location, and record the - migration in `BREAKING.md`. -- **Values-coverage audit**: every `.Values.*` path referenced by any template - must resolve in `values.yaml` (`scripts/check-values-coverage.py`, run by - `make lint-values-coverage` and CI). Strict schema + a missing key = a - silently unusable feature (the Authelia HPA-metrics trap). +18. **Selectors are immutable and minimal.** `authup.matchLabels` emits only + name, instance and component. User labels never enter selectors. Component + labels distinguish server, each console, worker and migration pods. +19. **Component names truncate before suffixing.** The suffix-specific budget + keeps every Service name and label value at 63 characters while preserving + existing valid resource names. Never widen the `min 52` budget without a + cross-revision name audit; a renamed kept Secret rotates credentials. +20. **The migration Job is pre-upgrade only.** Fresh installs need regular + backing resources before the server can initialize the database. On + upgrades, the Job runs before the rollout. The server sets + `MIGRATION_ENABLED=false` only during upgrades when this Job owns migration. +21. **Hook inputs must exist before regular resources.** The migration Job + 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. +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. + +## Network and platform behavior + +24. **Policies follow roles.** The server policy accepts enabled split consoles. + Console policies reach the server. Worker and migration policies provide + DNS plus release-local database/cache access when external egress is denied. + External services require the corresponding `extraEgress` rules. +25. **The migration policy is a hook.** A regular NetworkPolicy created after a + pre-upgrade Job cannot protect that Job. Keep its hook family and ordering + aligned with the migration Job for Helm and ArgoCD. +26. **Built-in stores are minimal conveniences.** PostgreSQL, MySQL and Valkey + are vendored single-instance StatefulSets on official images. Production + users should prefer external or operator-managed services. +27. **`global` stays schema-open.** Helm passes a parent's complete global map + into subcharts before schema validation. Closing this node makes the chart + unusable under unrelated umbrella globals. +28. **Every list and map passthrough is tpl-rendered.** Umbrella charts rely on + this. Free-form maps need `# @schema additionalProperties: true`. + +## Validation and generated artifacts + +- Cross-field rules and moved-value tombstones live in + `templates/validations.yaml`. +- `values.yaml` is the source for both generated README value tables and the + strict JSON schema. +- Every `.Values.*` template path must exist in `values.yaml`; the coverage + script enforces it. +- `scripts/check-beta64-contract.py` renders real manifests and asserts the + CLI, env, mount, routing, policy and migration contracts. Add an assertion + there before changing one of those boundaries. diff --git a/.agents/references/authup.md b/.agents/references/authup.md index 6c4110c..cdba7f4 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -1,106 +1,84 @@ -# authup (the application) +# authup application mapping -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.63 line (chart `appVersion`). +Repository: https://github.com/authup/authup. A local checkout commonly exists +at `/opt/projects/authup/authup`. This mapping is pinned to v1.0.0-beta.64, the +chart `appVersion`. -## Image / entrypoint contract +## Image and CLI -| Fact | authup source | Chart counterpart | +| Authup contract | Upstream source | Chart counterpart | |---|---|---| -| 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 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` | - -## server-core env surface - -Source of truth: `apps/server-core/src/app/modules/config/` -(`constants.ts` = `ConfigEnvironmentVariableName` enum, `read/env.ts`, -`normalize.ts` defaults + cross-field boot validation, `validator.ts`). -Docs mirror: `docs/src/guide/deployment/configuration-server-core*.md`. - -| authup env | Chart source | -|---|---| -| `DB_TYPE/HOST/PORT/USERNAME/PASSWORD/DATABASE` (mysql, postgres, better-sqlite3 only; sqlite forbidden in production) | `_database.tpl` dispatch + `authup.server.configEnv` / `secretEnv` | -| `REDIS` (bool or full URL; there is NO `REDIS_URL`) | `secret-redis.yaml` / `valkey/secret.yaml` connection-string key | -| `SMTP` (URL form; per-field SMTP is config-file-only) | `secret-smtp.yaml` | -| `PUBLIC_URL`, `TRUSTED_ORIGINS`, `TRUST_PROXY` (app default trusts every hop; chart pins "1") | `_urls.tpl` + `authup.server.configEnv` | -| `REGISTRATION_ENABLED`, `PASSWORD_RECOVERY_ENABLED`, `EMAIL_VERIFICATION_ENABLED`, `MFA_ENABLED`, `MFA_REQUIRED` (strict booleans: unparsable value crashes boot) | `server.features.*` / `server.mfa.*`, always quoted | -| `ACCOUNT_CONSOLE_ENABLED` (beta.62, default true): serves the `/account` self-service SPA off the IdP origin | `server.features.accountConsole` | -| `THEME_DIRECTORY_PATH` / `THEME_FRAGMENTS_ENABLED` (beta.59, EXPERIMENTAL): operator theme for the two served consoles; manifest at `/theme.json`, HTTP mount root is `/assets` only | `server.theme.*` (the chart composes theme.json) | -| `AUTH_CONSOLE_PATH` / `ACCOUNT_CONSOLE_PATH`: substitute a whole console package, boot-asserted `CONTRACT_VERSION` | deliberately NOT first-class; `server.config` + `extraVolumes` escape hatch | -| `USER_ADMIN_PASSWORD(_RESET)`, `CLIENT_SYSTEM_ENABLED/SECRET(_RESET)` | `auth.*` values + chart-managed secret | -| `SECRETS_ENCRYPTION_KEY` (base64 32 bytes; write-once, removal with wrapped rows fails loud) | `auth.secretsEncryptionKey(+Enabled)`, never generated, never optional | -| Cross-field boot validations (throttle needs event log, mfaRequired needs mfaEnabled, KEK length) | mirrored as render-time guards in `templates/validations.yaml` | -| `TRUSTED_ORIGINS` rejects `**` in a host at boot since beta.59 (`config/origins.ts`, `patternHasGlobstarInAuthority`); a single `*` is a supported host wildcard | `authup.assertTrustedOrigin` in `_urls.tpl`, asserted after tpl rendering | - -Config file: `authup.server.core.conf` in the process cwd -(`app/modules/config/read/fs.ts`; env always wins) -> `server.configuration` / -`server.existingConfigmap` mount. Provisioning files: -`/provisioning/*` scanned at boot, fail-closed -(`app/modules/provisioning/module.ts`) -> `server.provisioning.*` mount. - -## client-admin-console env surface - -Runtime config only (prebuilt Nitro bundle; bare `API_URL` etc. are build-time -and dead): `apps/client-admin-console/nuxt.config.ts`, -`docs/src/guide/deployment/configuration-client-admin-console.md`. -`NUXT_PUBLIC_API_URL` (browser-reachable server URL), `NUXT_PUBLIC_PUBLIC_URL`, -`NUXT_API_URL` (SSR-side override), `NUXT_PUBLIC_COOKIE_DOMAIN` (deliberately -never set by the chart: sharing a cookie domain with the server origin is -unsupported per `.agents/architecture.md` in the monorepo), -`NUXT_PUBLIC_CLIENT_ID` (beta.59+, defaults to the per-realm built-in -`admin-console` client; fork-only override, reachable via -`adminConsole.config`). Chart counterpart: `_admin-console-env.tpl`. - -## Operational contract - -- `GET /` = anonymous status endpoint `{version, date, features}` - (`adapters/http/controllers/workflows/status/`) -> liveness/readiness for - server-core; client-admin-console uses its SSR `/`. -- server-core auto-runs migrations + provisioning at boot - (`app/modules/database/module.ts`; no off-switch) -> generous startupProbe; - optional pre-upgrade migration Job for multi-replica DDL serialization. -- `migration run` (`cli/commands/migration.ts`, `defineCLIMigrationCommand`) - builds only three modules: config, logger, database. No http, cache, mail, - identity or provisioning module. It therefore ignores `REDIS` / `SMTP`, and - never scans `/provisioning` (`ProvisionerModule` is registered by - `createApplication()`, i.e. the `start` command only) -> the chart drops all - three from the migration Job. -- `migration run` DOES read the config file, unconditionally: `createCLIConfigModule` - passes `fs: {}` (truthy) into `readConfig`, so `readConfigRawFromFS` runs - (`config/read/fs.ts`). Env wins per key, but the db keys typeorm-extension's - env reader does not name survive: `ssl`, `socketPath`, `replication`, - `poolSize`, `charset`, `extensions` (postgres `CREATE EXTENSION` during - `initialize()`). So the config file decides how the migration connects and - what it creates -> the chart MUST mount it on the Job. (`entities` and - `subscribers` are NOT in that set: `DB_ENTITIES` / `DB_SUBSCRIBERS` exist. - Dump the real list with - `grep -rhoE "DB_[A-Z_]+" node_modules/typeorm-extension/dist | sort -u`.) -- Under `NODE_ENV=production` (baked into the image) `migration run` needs the - writable directory before it touches the database: the logger adds winston - File transports for `/http.log` and `/error.log`, and the - transport does `mkdirSync` + open eagerly. An unwritable path is a hard ENOENT - failure of the command, not a degradation -> the Job keeps the writable mount. -- Replicas > 1 without redis: per-process MemoryCache breaks auth codes, - revocations, MFA challenges (`app/modules/cache/module.ts`) -> hard - validation in the chart. -- `GET /metrics` is unauthenticated (`middlewarePrometheus` default on) -> - ServiceMonitor targets the Service; ingress warning in values/NOTES. -- In-process cron sweepers (oauth2-cleaner, event-cleaner) are idempotent - deletes; no leader election needed. -- Reserved client names: `admin-console` and `account-console` are provisioned - as built-in system clients in EVERY realm and take over a pre-existing client - of that name (beta.59). The shared per-realm `web` client was removed in the - same release; `TRUSTED_ORIGINS` now feeds the system clients' redirect - allowlists. NOTES warns against declaring either name in - `server.provisioning`. -- beta.60 ships a heavy migration (140 indexes, MySQL `varchar(36)` -> - `varchar(255)` table rewrites, three dropped tables) and beta.62 adds a - unique constraint on `auth_identity_provider_accounts` that ABORTS the boot - on pre-existing duplicates. Both are arguments for - `server.migration.enabled` on an upgrade, not just for multi-replica. +| One `authup/authup` image with direct CLI args | `Dockerfile`, `entrypoint.sh` | `authup.appImage`; every application Deployment | +| Combined service: `start` | `apps/authup/src/commands/start.ts` | `server/deployment.yaml` default | +| API only: `start core` | `apps/authup/src/module.ts`, command tests | server when `server.splitConsoles=true` | +| Split consoles: `start console auth|admin|account` | `apps/authup/src/console/`, `apps/server-*-console/` | the three console directories | +| Background worker: `start worker` | `apps/authup/src/module.ts`, `apps/server-core/src/app/modules/components/module.ts` | `worker/deployment.yaml` | +| Migration: `migration run` | `apps/server-core/src/cli/commands/migration.ts` | `server/migration-job.yaml` | +| Core port 3000; console ports 3020/3021/3022 | `packages/server-config/src/sections/*/schema.ts`, `Dockerfile` | role `containerPorts` and Services | + +Do not use the pre-beta.64 prefixes `server/core` and +`client/admin-console`. A worker has no HTTP listener; it must not gain a +Service or HTTP probes. + +## Unified configuration + +The single schema is in `packages/server-config/src/`. Every service reads one +`authup.yml`; each role selects its relevant sections. + +| Authup setting | Upstream source | Chart counterpart | +|---|---|---| +| config file `authup.yml` | `packages/server-config/src/read/fs.ts` | `/etc/authup/authup.yml`, key `authup.yml` | +| `PUBLIC_URL`, `INTERNAL_URL` | `sections/root/schema.ts`, `helpers/public-url.ts` | `_urls.tpl`, `_console-env.tpl` | +| `LOG_DIRECTORY_PATH` | `sections/core/schema.ts`, Dockerfile | `/var/log/authup` log emptyDir | +| `PROVISIONING_DIRECTORY_PATH` | `sections/core/schema.ts`, Dockerfile | `/etc/authup/provisioning` | +| `WORKER_ENABLED`, `MIGRATION_ENABLED` | `sections/core/schema.ts` | explicit role ownership in Deployments | +| `AUTH_CONSOLE_PORT`, `ADMIN_CONSOLE_PORT`, `ACCOUNT_CONSOLE_PORT` | console section schemas | `_console-env.tpl` | +| database, Redis, SMTP and strict feature flags | section schemas and `constants.ts` | `_server-env.tpl`, `_database.tpl`, Secrets | + +Environment values win over file values. File-only database options still make +the config mount load-bearing for `migration run`. + +## Role boundaries + +- `start` combines core, enabled consoles and worker behavior in one process. +- `start core` serves the API only. If a dedicated worker exists, the API must + receive `WORKER_ENABLED=false`. +- `start worker` requires `WORKER_ENABLED=true` and database/cache config. It + does not need SMTP, bootstrap identity secrets, HTTP probes, or migrations. +- Split consoles use the deployment-wide `PUBLIC_URL`. Server-side console + calls use `INTERNAL_URL`, which the chart points at the core Service. +- `ACCOUNT_CONSOLE_ENABLED` and `ADMIN_CONSOLE_ENABLED` describe whether those + surfaces are available. The chart maps them from the top-level console + `enabled` values in both combined and split modes. + +## Migrations and provisioning + +Core startup can initialize and migrate the database. The chart therefore lets +fresh installs boot normally after built-in backing services are created. The +optional migration Job is pre-upgrade only and sets `MIGRATION_ENABLED=false` +on upgrade server pods so ownership is not duplicated. + +The migration command constructs configuration, logger and database modules. +It does not need provisioning, Redis, SMTP, HTTP, or bootstrap identity data. +It does need database credentials and `authup.yml`; the chart gives it a +hook-scoped config copy because pre-upgrade hooks run before regular release +resources. + +Provisioning files are read from `PROVISIONING_DIRECTORY_PATH` at core startup. +The chart mounts them read-only at `/etc/authup/provisioning`. Reserved client +names `admin-console` and `account-console` remain application-owned system +clients and must not be declared as user provisioning entries. + +## Security and availability facts + +- The production image cannot use SQLite, so a real database is mandatory. +- `REDIS` is the connection variable; `REDIS_URL` is not part of the contract. +- More than one API replica needs shared Redis for authorization codes, + revocations and MFA challenges. +- `SECRETS_ENCRYPTION_KEY` is a base64 32-byte write-once key. Loss or rotation + makes wrapped MFA and signing material unreadable. +- `GET /` is the anonymous core health endpoint. `/metrics` is unauthenticated + when enabled. +- Split console prefixes must be removed before requests reach their listeners. + Admin/account login start and callback endpoints are core API routes, not + console asset routes. diff --git a/.agents/structure.md b/.agents/structure.md index 140e887..7cb674c 100644 --- a/.agents/structure.md +++ b/.agents/structure.md @@ -1,79 +1,69 @@ # Project Structure -Standard chart-releaser / chart-testing repository shape: charts under +Standard chart-releaser and chart-testing repository shape: charts under `charts/`, per-chart tooling, repo-level CI and release automation. ``` authup/helm -├── DESIGN.md # authoritative design record (read first) -├── Makefile # docs / schema / lint / template / coverage; identical locally and in CI -├── scripts/check-values-coverage.py # .Values.* path audit (see architecture.md) -├── release-please-config.json # two packages: root (simple) + charts/authup (helm), linked-versions +├── DESIGN.md # authoritative design record +├── Makefile # docs, schema, lint, render, contract checks +├── scripts/ +│ ├── check-values-coverage.py # every template value exists in values.yaml +│ └── check-beta64-contract.py # executable app/chart integration contract +├── release-please-config.json # linked root + chart releases ├── .release-please-manifest.json -├── version.txt # root package version, kept in step with the chart -├── CHANGELOG.md # root package changelog (GENERATED by release-please) -├── renovate.json # helm-values + github-actions managers, conventional commits -├── .github/ -│ ├── configs/{ct.yaml, lintconf.yaml} -│ └── workflows/{lint-test.yaml, release.yaml} +├── version.txt +├── CHANGELOG.md # GENERATED by release-please └── charts/authup/ - ├── Chart.yaml # version owned by release-please; appVersion tracks authup - ├── values.yaml # single source: "# --" helm-docs comments + "# @schema" blocks - ├── values.schema.json # GENERATED (dadav/helm-schema) - ├── README.md # GENERATED (helm-docs) from README.md.gotmpl + values comments + ├── Chart.yaml # version is release-please-owned; appVersion tracks Authup + ├── values.yaml # helm-docs comments + helm-schema blocks + ├── values.schema.json # GENERATED by dadav/helm-schema + ├── README.md # GENERATED by helm-docs ├── README.md.gotmpl - ├── BREAKING.md # value-migration ledger for the 0.x line - ├── ci/ # ct install scenario matrix, one install per *-values.yaml - │ ├── default-values.yaml # built-in postgres, both services + ├── BREAKING.md # migration ledger for the 0.x line + ├── ci/ + │ ├── default-values.yaml # combined server + built-in PostgreSQL │ ├── mysql-values.yaml - │ ├── external-db-values.yaml # externalDatabase + existingSecret, against ci/manifests fixture - │ ├── valkey-values.yaml # cache + 2 replicas + migration hook - │ ├── server-only-values.yaml # headless IdP (adminConsole.enabled=false) - │ └── manifests/postgres.yaml # fixtures pre-applied before ct install + │ ├── external-db-values.yaml + │ ├── valkey-values.yaml # replicas + migration hook + │ ├── server-only-values.yaml # optional admin/account surfaces disabled + │ ├── split-values.yaml # split consoles + worker + restrictive policies + │ ├── theme-values.yaml + │ └── manifests/postgres.yaml └── templates/ - ├── _helpers.tpl # names, labels, images, tplvalues, affinity, securityContext - ├── _secrets.tpl # lookup-or-generate + auth/smtp secret resolution - ├── _database.tpl # engine dispatch (postgres/mysql/external) + redis helpers - ├── _urls.tpl # publicUrl/apiUrl/origin derivation + post-render scheme asserts - ├── _ingress.tpl # shared Ingress + HTTPRoute renderers (server and ui call them) - ├── _server-env.tpl # configEnv map, secretEnv list, shared volumes (deployment + job) - ├── _admin-console-env.tpl # ui configEnv map - ├── validations.yaml # render-nothing fail-fast guards (cross-field rules) - ├── secret.yaml # chart-managed auth secret (admin password, system client, KEK) - ├── secret-db.yaml # external-db password secret (no generation fallback) - ├── secret-redis.yaml # external redis connection-string secret - ├── secret-smtp.yaml - ├── serviceaccount.yaml # one SA shared by both services - ├── extra-list.yaml # extraDeploy passthrough - ├── NOTES.txt # computed URLs, credential retrieval, operational warnings - ├── server/ # server-core: deployment, service, ingress, httproute, - │ # configmap-env, configmap-configuration, configmap-provisioning, - │ # migration-job, hpa, pdb, networkpolicy, servicemonitor - ├── ui/ # client-admin-console: deployment, service, ingress, httproute, - │ # configmap-env, hpa, pdb, networkpolicy - ├── postgresql/ # built-in instance: statefulset, service, secret - ├── mysql/ # built-in instance: statefulset, service, secret - └── valkey/ # built-in instance: statefulset, service, secret + ├── _helpers.tpl # names, labels, image, tpl rendering + ├── _secrets.tpl # lookup-or-generate and secret resolution + ├── _database.tpl # database and Redis dispatch + ├── _urls.tpl # shared public URL and origin helpers + ├── _ingress.tpl # shared Ingress and HTTPRoute primitives + ├── _server-env.tpl # server-role env, secrets, mounts and volumes + ├── _console-env.tpl # split-console env and mounts + ├── validations.yaml # fail-fast cross-field and migration guards + ├── server/ # combined/core API, migration and public routing + ├── auth-console/ # split `start console auth` resources + ├── admin-console/ # split `start console admin` resources + ├── account-console/ # split `start console account` resources + ├── worker/ # `start worker`; no Service or HTTP probes + ├── postgresql/ # vendored single-instance backing service + ├── mysql/ + └── valkey/ ``` -## Two components, per-role template directories +## Role directories -`server/` (server-core, the IdP: OAuth2/OIDC surface + SSR auth pages) and -`ui/` (client-admin-console admin console) are separate template directories with ~85% -similar deployment templates. This duplication is DELIBERATE: authentik built -the DRY role-loop and reverted it ("takes DRY maybe a bit too far", their -PR #163). Do not introduce a role loop. A future authup server/worker split -becomes a third directory with the same skeleton. +All application roles use the same `authup/authup` image. The default server +runs `start`, which combines the API and consoles. `server.splitConsoles=true` +switches it to `start core` and enables explicit console Deployments with ports +3020, 3021 and 3022. `worker.enabled=true` adds `start worker`. -Both services run the SAME image (`authup/authup`) with different args -(`server/core start` vs `client/admin-console start`). The image entrypoint force-exports -`PORT=3000` for both, so `containerPort` is pinned to 3000 everywhere and only -Service ports are values. +The similar role directories are deliberate. Authup roles have different +ports, probes, secrets and Services, and authentik reverted a generic role loop +after it obscured those differences. Keep sibling templates explicit and make +mechanical changes consistently across them. ## Built-in backing services -`postgresql/`, `mysql/`, `valkey/` are vendored minimal single-instance -StatefulSets on docker-official images, NOT subcharts. They are a dev / -small-production convenience; production users bring `externalDatabase` / -`externalRedis` or an operator. See DESIGN.md for why no bitnami (or any) -subchart dependency exists. +`postgresql/`, `mysql/` and `valkey/` are minimal StatefulSets using official +images, not subcharts. They are for development and small deployments. +Production users normally supply `externalDatabase`, `externalRedis`, or an +operator-managed service. diff --git a/.agents/testing.md b/.agents/testing.md index 9d4cd59..8ba1a82 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -1,190 +1,100 @@ # Testing -No unit-test framework (cohort norm: neither authelia nor authentik ship -helm-unittest). The safety net is layered rendering plus a kind install -matrix. +The safety net is layered rendering plus chart-testing installs on kind. -## Layers +## Local layers -| Layer | Command | What it catches | +| Layer | Command | Coverage | |---|---|---| -| helm lint + ct lint | `make lint` | schema violations, yamllint, Chart.yaml shape | -| Render matrix | `make template` | template errors across every `ci/*-values.yaml` | -| Values coverage | `make lint-values-coverage` | `.Values.*` paths missing from values.yaml (strict-schema dead features) | -| Drift gates (CI) | `make docs` / `make schema` + `git status --porcelain` | uncommitted regenerations of README.md / values.schema.json | -| ct install (CI) | kind cluster, per `ci/*-values.yaml`: install, plus two upgrades | real boot: DB provisioning, probes, migrations, and pre-upgrade hooks | +| Helm and ct lint | `make lint` | schema, YAML and chart metadata | +| CI render matrix | `make template` | every `ci/*-values.yaml` permutation | +| Values coverage | `make lint-values-coverage` | template paths missing from values.yaml | +| beta.64 contract | `make lint-beta64-contract` | roles, env, mounts, routing, policies and guards | +| Full local suite | `make test` | all four layers above | +| Generated drift | `make docs schema` | README.md and values.schema.json | -`make test` runs lint + template + coverage locally. +The contract script uses real `helm template` output and PyYAML. Prefer adding +a focused assertion there over brittle text grep when changing the application +boundary. -## Rendering permutations by hand +## Useful renders ```bash -helm template test charts/authup # defaults (built-in postgres) -helm template test charts/authup -f charts/authup/ci/valkey-values.yaml -helm template test charts/authup --set server.ingress.enabled=true \ - --set server.ingress.hostname=auth.example.com --set server.ingress.tls=true \ - --set adminConsole.ingress.enabled=true --set adminConsole.ingress.hostname=app.example.com --set adminConsole.ingress.tls=true +helm template test charts/authup +helm template test charts/authup -f charts/authup/ci/split-values.yaml +helm template test charts/authup -f charts/authup/ci/valkey-values.yaml --is-upgrade +python3 scripts/check-beta64-contract.py charts/authup all ``` -When verifying env wiring, grep the rendered ConfigMaps/Deployments for -`PUBLIC_URL`, `TRUSTED_ORIGINS`, `NUXT_PUBLIC_API_URL`, `DB_*`, `REDIS`, -`SMTP`, and check every `secretKeyRef` points at a Secret the same render -actually creates. +The split fixture is the high-value beta.64 scenario: separate core/auth/admin/ +account roles, a worker, migration hooks, one-origin routing, built-in database +and cache, plus restrictive NetworkPolicies. -## Negative tests are part of the contract +## Negative contracts -`templates/validations.yaml` guards must FAIL these renders; when touching -validations or the values they read, re-run the battery: +`templates/validations.yaml` must fail these classes of input: -```bash -helm template t charts/authup --set postgresql.enabled=false # no db -helm template t charts/authup --set mysql.enabled=true # both dbs -helm template t charts/authup --set server.replicaCount=2 # replicas w/o cache -helm template t charts/authup --set server.mfa.required=true # mfa.required w/o enabled -helm template t charts/authup --set auth.existingSecret=x --set auth.adminPassword=y -helm template t charts/authup --set server.publicUrl=auth.example.com # scheme-less URL -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.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.configuration=logger: true' --set server.existingConfigmap=cm # both config carriers -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/ -helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=assets/logo.svg # asset missing from files -helm template t charts/authup --set server.theme.enabled=true --set 'server.theme.tokens.--authup-bg=url(x)' # token value authup rejects -helm template t charts/authup --set 'server.trustedOrigins[0]=https://**.x' # globstar host -helm template t charts/authup --set server.route.enabled=true --set server.publicUrl=https://h.x/auth # sub-path route without matches -helm template t charts/authup --set server.route.enabled=true --set server.ingress.enabled=true \ - --set server.ingress.hostname=h.x --set server.ingress.path=/auth # same, via the derived URL -``` - -The route guard reads the public URL AFTER derivation, so the ingress-derived -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 -``` +- no database, both built-in databases, or an unknown external database type +- API replicas/HPA without Redis +- MFA required while MFA is disabled +- inline and existing-secret carriers set together +- scheme-less public URLs or an Ingress without a hostname +- a `server.config` key owned by a first-class value +- both inline and existing configuration ConfigMaps +- invalid route flags, including flags on disabled roles +- an HTTPRoute catch-all created accidentally from a sub-path public URL +- `server.splitConsoles=true` without the server or auth console +- non-empty `server.features.accountConsole`, which moved to + `accountConsole.enabled` +- invalid theme manifests or dangerous trusted-origin globstars -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. +The beta.64 contract script exercises the moved value, split dependencies, +route flags and reserved role env variables directly. -The pre-upgrade migration Job must stay narrower than the Deployment. Helm -applies a hook before the release manifest, so anything the Job references has -to exist from the previous release: +## Hook checks -```bash -helm template t charts/authup --set server.migration.enabled=true \ - --set valkey.enabled=true --set smtp.connectionString=smtp://u:p@mail:25 \ - --set auth.systemClientEnabled=true \ - --set server.provisioning.enabled=true --set 'server.provisioning.files.realms\.json=[]' \ - --set 'server.configuration=db: {ssl: true}' \ - -s templates/server/migration-job.yaml -``` - -The Job's only secret-backed env must be `DB_PASSWORD` (plus -`SECRETS_ENCRYPTION_KEY` when the KEK is set): no `REDIS`, no `SMTP`, no -`USER_ADMIN_PASSWORD`, no `CLIENT_SYSTEM_SECRET`. Volumes `writable` / `tmp` / -`configuration` but NO `provisioning`; and the configuration volume must name -`-server-migration-configuration` -(the hook-scoped copy at weight -5), never `-server-configuration`. The -server Deployment in the same render must still carry all of them. Dropping the -config file from the Job is NOT a valid simplification: `migration run` reads it -and its file-only db keys (`ssl`, `socketPath`, `extensions`) govern the -connection, so a missing mount migrates over a plaintext connection instead of -failing. - -Names have two ceilings, not one (rule 9). 63 applies to a Service (DNS-1035 -label) and to a Job (its name becomes a `job-name` label value); 253 applies to -ConfigMaps and Secrets. Audit every rendered name at the longest release name -helm accepts: - -```bash -helm template $(python3 -c "print('n'*53)") charts/authup \ - --set valkey.enabled=true --set server.migration.enabled=true | python3 -c " -import sys, yaml -for d in yaml.safe_load_all(sys.stdin): - if d and d['kind'] in ('Service','Job') and len(d['metadata']['name']) > 63: - print('OVER 63:', d['kind'], d['metadata']['name']) -" -``` +The pre-upgrade migration Job must stay narrower than the server Deployment: -Must print nothing. The stronger property, and the one to assert whenever the -budget in `authup.component.fullname` changes, is that **no name changes for a -release that could already install**: render every release-name length 3..53 on -both `origin/master` and the branch, and check that the two name sets differ only -at lengths where master already emitted an over-63 Service or Job. Widening the -budget silently renames resources, and a renamed `resource-policy: keep` Secret -regenerates the admin password. +- args are exactly `migration run` +- only database password and an optional encryption key are secret-backed +- no Redis, SMTP, bootstrap identity secret, or provisioning mount +- `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 +- fresh-install server env has no `MIGRATION_ENABLED`; upgrade server env has + `MIGRATION_ENABLED=false` when the Job is enabled -`useHelmHooks=false` must print the Flux/plain-helm warning in NOTES.txt, and -must not print it with hooks on. NOTES is not reachable through `helm template`, -and `.Files.Get "templates/NOTES.txt"` does NOT work either (helm excludes -`templates/` from `.Files`, so the wrapper renders empty and BOTH directions -"pass"). Inline the raw template text into a generated template instead: +Run both Helm and ArgoCD annotation paths: ```bash -cp -r charts/authup /tmp/nc -{ printf 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: notes\ndata:\n notes: |\n'; \ - sed 's/^/ /' /tmp/nc/templates/NOTES.txt; } > /tmp/nc/templates/zz-notes.yaml -helm template t /tmp/nc --set server.migration.enabled=true --set useHelmHooks=false \ - -s templates/zz-notes.yaml | grep -c 'useHelmHooks=false' # must be >0 -helm template t /tmp/nc --set server.migration.enabled=true \ - -s templates/zz-notes.yaml | grep -c 'useHelmHooks=false' # must be 0 +helm template t charts/authup -f charts/authup/ci/split-values.yaml --is-upgrade +helm template t charts/authup -f charts/authup/ci/split-values.yaml \ + --set useHelmHooks=false --is-upgrade ``` -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 -global key as the cheap in-repo version of that check. +## Name safety -A single `*` host wildcard (`https://*.example.com`) must still RENDER: authup -supports it, only `**` is the allow-any-origin trap. +Service names and label values stop at 63 characters. Whenever the component +fullname budget changes, render release-name lengths 3 through 53 on both +`origin/master` and the branch. No name may change for a release that previously +produced valid resources. This is stricter than checking only maximum length: +renaming a kept Secret can rotate credentials. -The `server.theme` manifest guards assert the value AFTER `tpl` rendering, so -both directions need a case: a templated token or asset path must RENDER, and -one whose rendered result is illegal must FAIL. Validating the raw value gets -this backwards in a way that looks correct (every `{{ ... }}` contains `}`, so -the forbidden-character check rejects it for the wrong reason). +## Generated-schema checks -The generated `values.schema.json` must keep catching typos -(`--set server.replicaCountt=3` fails) while free-form maps stay open -(`--set server.config.X=y`, `--set server.resources.limits.cpu=1` succeed). +After values changes, run `make docs schema`. The schema must reject typos such +as `server.replicaCountt` while allowing annotated free-form maps such as +`server.config` and resource limits. Keep the stray global key in the default CI +fixture because it covers umbrella-chart schema behavior. ## ct install specifics -- Scenario matrix = `charts/authup/ci/*-values.yaml`; each file must make the - chart actually installable on kind (persistence off, small resources, - explicit fixtures). -- `ci/manifests/` is pre-applied into the test namespace before `ct install` - (the external-db scenario's throwaway postgres + secrets live there). -- The kind job only runs when `ct list-changed` reports chart changes, so - docs-only PRs stay fast. -- `upgrade: true` (in `.github/configs/ct.yaml`) is what puts the pre-upgrade - migration Job on a real cluster at all: a plain `helm install` skips - `pre-upgrade` hooks entirely, so without it the Job and its hook-scoped - ConfigMap are render-tested only. Per values file ct then runs the chart on - `master` and upgrades to this revision, then installs this revision and - upgrades it to itself. The first leg is skipped once a release bumps the - middle digit, because ct reads that as a breaking change for a 0.x chart - (`~0.x.y` constraint); the self-upgrade leg always runs. Budget roughly 3x - the install-only runtime. -- `--timeout 600s` is passed to install AND upgrade (ct hands `helm-extra-args` - to both), so it also has to cover hook execution. It accounts for first-pull - of the authup image plus boot-time migrations; server-core's startupProbe budget (60 x 5s) covers create-db + - migrate + provision on first boot. +- Every `charts/authup/ci/*-values.yaml` file must be installable on kind with + small resources and persistence disabled. +- `ci/manifests/` is pre-applied for the external-database fixture. +- `upgrade: true` runs native pre-upgrade hooks. A plain install never runs them. +- The first master-to-branch upgrade may be skipped for a breaking 0.x release; + the self-upgrade still runs. +- The 600-second timeout includes image pulls, database startup, migrations and + the server startup-probe budget. diff --git a/AGENTS.md b/AGENTS.md index 1643290..1abb1ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,10 +3,9 @@ # authup/helm - Agent Guide Helm charts for [Authup](https://authup.org), an authentication & authorization -system. One application chart today: `charts/authup` deploys the two runtime -services of the [authup monorepo](https://github.com/authup/authup) (server-core -IdP/API and the client-admin-console admin UI) plus optional built-in PostgreSQL, MySQL -and Valkey instances. +system. One application chart today: `charts/authup` deploys Authup's combined +server by default, or separate core, auth/admin/account console and worker roles, +plus optional built-in PostgreSQL, MySQL and Valkey instances. `DESIGN.md` at the repo root is the authoritative design record: every major decision with the evidence it rests on (deep-dives into authelia/chartrepo, @@ -17,7 +16,7 @@ changing chart architecture. ## Quick Reference ```bash -make test # lint + render every ci/*-values.yaml + values-coverage audit +make test # lint + render matrix + coverage + beta.64 contract make lint # helm lint + ct lint make template # render the chart once per ci/*-values.yaml file make docs # regenerate charts/*/README.md (helm-docs, dockerized) @@ -26,6 +25,7 @@ make lint-values-coverage # every .Values.* in templates must resolve in valu helm template test charts/authup # quick render helm template test charts/authup -f charts/authup/ci/mysql-values.yaml +helm template test charts/authup -f charts/authup/ci/split-values.yaml ``` - **helm** >= 3.14 and **docker** (for the pinned generator images) required. @@ -47,9 +47,9 @@ helm template test charts/authup -f charts/authup/ci/mysql-values.yaml `fix(authup): ...`. release-please (`release-type: helm`) owns `Chart.yaml` `version`, `CHANGELOG.md` and `.release-please-manifest.json`; never bump them by hand. -- Publishing is chart-releaser on every master push (idempotent via - `CR_SKIP_EXISTING`): GitHub release `authup-` + `index.yaml` on the - `gh-pages` branch + OCI push to `oci://ghcr.io/authup/helm`. +- Publishing is hevi/chart-releaser on every master push: GitHub release + `authup-`, `index.yaml` on `gh-pages`, and an OCI push to + `oci://ghcr.io/authup/helm`. - Do NOT add `Co-Authored-By: Claude ...` or any AI-attribution trailer to commits, issues or PRs. This overrides default agent-tooling guidance. diff --git a/DESIGN.md b/DESIGN.md index 38cbee1..b7b06fb 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,508 +1,249 @@ -# Authup Helm Chart Repository — Design +# Authup Helm Chart Design -Status: draft for review. Distilled from deep-dives into `authelia/chartrepo`, -`goauthentik/helm`, `PrivateAIM/helm` (both authup deployments), `bitnami/charts` -(common + keycloak + postgresql + redis), and the authup monorepo's own deployment -surface (Dockerfile, config module, docs). Full research reports live outside the -repo; every decision below cites its evidence. +Status: implemented. This is the authoritative architecture record for the +chart. It incorporates evidence from the Authup monorepo and comparative +reviews of authelia/chartrepo, goauthentik/helm, bitnami/charts and +PrivateAIM/helm. ## 1. Goals and constraints -1. **Bitnami-grade flexibility without bitnami dependencies.** The chart adopts the - bitnami values UX (`existingSecret` + key mapping, `extraEnvVars` / - `extraEnvVarsCM` / `extraEnvVarsSecret`, `extraVolumes`, `initContainers`, - `sidecars`, `extraDeploy`, `commonLabels` / `commonAnnotations`, - `fullnameOverride`, `diagnosticMode`, multi-release-per-namespace naming) but - depends on **zero** external charts. Rationale: the Broadcom/bitnami licensing - change made bitnami subcharts effectively unusable (PrivateAIM is actively - migrating off them; authentik must pin docker-official images plus - `global.security.allowInsecureImages: true` just to keep the postgres subchart - alive), authentik's k8s-at-home common-library era ended when that library died - upstream, and Authelia removed its redis/postgres/mariadb subcharts in 0.11.0 - after years of maintenance. The ~5 naming/label/secret helpers worth having are - vendored locally (Apache-2.0-clean, bitnami-compatible key names). -2. **Two workloads, one chart, one image.** `authup/authup` is a single image whose - entrypoint dispatches on args: `server/core start` (server-core, the IdP/API) and - `client/admin-console start` (client-admin-console, the Nuxt admin UI). The `authup` CLI supervisor is - **not routable through the container entrypoint** and the monorepo docs pin - "containers with one service each" as the production topology — so the chart - ships two Deployments and never a combined pod. -3. **The container port is always 3000 for both services.** The entrypoint - force-exports `PORT=3000` / `NUXT_PORT=3000`; a chart-set `PORT` env is dead. - `containerPort` is pinned, only Service ports are values. -4. **Hybrid database model: postgres AND mysql are first-class** (`DB_TYPE` - supports both), with in-cluster dev/small-prod provisioning for either engine and - `externalDatabase` as the documented production path. sqlite is impossible on the - published image (`NODE_ENV=production` is baked in and production forbids - sqlite), so the chart hard-fails when no database is configured. -5. **Solo-maintainable repo.** release-please (`release-type: helm`) owns version - bumps (tada5hi already operates exactly this in PrivateAIM/helm); generated - README + values.schema.json with CI drift gates; the whole pipeline runnable - locally through a Makefile. - -## 2. Repository layout - -``` -authup/helm -├── charts/authup/ -│ ├── Chart.yaml # apiVersion v2, dependencies: [] — self-contained -│ ├── values.yaml # "# --" helm-docs comments + "# @schema" blocks -│ ├── values.schema.json # GENERATED (dadav/helm-schema), drift-gated -│ ├── README.md # GENERATED (helm-docs), drift-gated -│ ├── README.md.gotmpl -│ ├── BREAKING.md # value-migration ledger for the 0.x line -│ ├── CHANGELOG.md # release-please owned -│ ├── .helmignore -│ ├── ci/ # ct install scenario matrix (one install per file) -│ │ ├── default-values.yaml # bundled postgres, both services -│ │ ├── mysql-values.yaml # bundled mysql -│ │ ├── external-db-values.yaml # externalDatabase + existingSecret fixture -│ │ ├── redis-values.yaml # bundled valkey + replicas 2 -│ │ └── server-only-values.yaml # adminConsole.enabled=false (headless deployment) -│ ├── ci/manifests/ # fixtures pre-applied before ct install -│ └── templates/ -│ ├── _helpers.tpl # names, labels, images (vendored bitnami-compatible) -│ ├── _secrets.tpl # lookup-based generate-if-absent + existingSecret resolution -│ ├── _database.tpl # engine dispatch helpers (host/port/type/secret) -│ ├── _urls.tpl # publicUrl / apiUrl / origin derivation -│ ├── validations.yaml # render-nothing fail-fast cross-field guards -│ ├── secret.yaml # chart-managed auth secret -│ ├── secret-db.yaml # external-db password fallback secret -│ ├── secret-redis.yaml -│ ├── configmap-provisioning.yaml -│ ├── serviceaccount.yaml -│ ├── extra-list.yaml # extraDeploy passthrough -│ ├── NOTES.txt -│ ├── server/ # server-core: deployment, service, ingress, -│ │ # httproute, configmap-env, migration-job, hpa, -│ │ # pdb, networkpolicy, servicemonitor -│ ├── ui/ # client-admin-console: deployment, service, ingress, -│ │ # httproute, configmap-env, hpa, pdb, networkpolicy -│ ├── postgresql/ # optional built-in dev instance (statefulset, -│ │ # service, secret) — docker-official image -│ ├── mysql/ # optional built-in dev instance — docker-official image -│ └── valkey/ # optional built-in cache instance -├── .github/ -│ ├── configs/{ct.yaml, lintconf.yaml, chart_schema.yaml} -│ └── workflows/{lint-test.yaml, release.yaml} -├── release-please-config.json + .release-please-manifest.json -├── renovate.json -├── Makefile # docs / schema / lint / template targets == CI -├── CONTRIBUTING.md -└── README.md # repo-level: install one-liner, links -``` - -Single chart today, but the `charts/` + per-chart tooling layout is what `ct`, -chart-releaser, helm-docs and renovate all assume, and it leaves room for future -charts (e.g. an `authup-remote` RBAC chart, authentik-style). - -## 3. Chart architecture - -### 3.1 Workloads - -| | `server` (server-core) | `ui` (client-admin-console) | -|---|---|---| -| args | `["server/core", "start"]` | `["client/admin-console", "start"]` | -| containerPort | 3000 (pinned) | 3000 (pinned) | -| role | OAuth2/OIDC IdP origin + SSR auth pages | admin console, ordinary OAuth2 RP | -| state | stateless w/ external DB+redis | fully stateless | -| probes | httpGet `/` (status endpoint); generous startupProbe (boot = migrate + provision) | httpGet `/` | -| scaling | replicas > 1 **requires redis** (hard template fail) | free | -| default | enabled | enabled (`adminConsole.enabled: false` = headless IdP) | - -Per-role template directories, ~85% duplication between the two deployment -templates **accepted deliberately** — authentik tried the DRY role-loop and -reverted it ("takes DRY maybe a bit too far"); with genuinely heterogeneous roles -the explicit files stay greppable and modifiable. - -The structure is worker-ready: when authup's server/worker split lands, it becomes -a third directory `templates/worker/` with the same skeleton. - -### 3.2 Naming, labels, multi-release - -Vendored helpers (bitnami-compatible semantics, local implementation): - -- `authup.fullname` — release-scoped, honors `fullnameOverride` (bitnami key name, - not Authelia's confusing nameOverride-acts-as-fullname variant), 63-char safe. -- `authup.server.fullname` / `authup.adminConsole.fullname` — `-server` / `-ui`. -- `authup.labels.standard` / `authup.labels.matchLabels` — the five - `app.kubernetes.io/*` labels; selectors carry ONLY name+instance+component - (user `commonLabels` never leak into immutable selectors — the bitnami `pick` - guard). `app.kubernetes.io/component: server|ui` separates the two Services' - selectors within one release. -- Zero literal names anywhere; NetworkPolicies scoped by instance labels; no - cluster-scoped resources. Two releases in one namespace need zero overrides — - the PrivateAIM 13-row "must match" secret-name table is the anti-pattern this - kills. - -### 3.3 Config model: explicit env, not a config-file mirror - -Authup is env-configured (env beats config file), so the chart renders env vars and -**never re-templates authup's config schema** — Authelia's 714-line configMap with -~30 `semverCompare` version gates is the negative print (a chart PR per app -release, plus their verified schema-drift bugs). Three layers: - -1. **First-class values** for the load-bearing ~15 options, rendered into a - per-role env ConfigMap (`-server-env`): `PUBLIC_URL`, - `TRUSTED_ORIGINS`, `TRUST_PROXY`, feature flags - (`REGISTRATION_ENABLED`, `PASSWORD_RECOVERY_ENABLED`, - `EMAIL_VERIFICATION_ENABLED`), MFA block, event-log block, token TTLs, - bootstrap toggles. Strict-boolean vars (`EVENT_LOG_*`, `MFA_*`, - `LOGIN_ATTEMPT_THROTTLE_ENABLED`) always render `quote`d — authup's - `readBoolStrict` crashes the pod on sloppy values. -2. **Secrets via `valueFrom.secretKeyRef`** (authup has no `*_FILE` support, so no - file-mounted secrets): `DB_PASSWORD`, `REDIS` (URL form embeds the password — - the whole connection string lives in a Secret, the flame-hub - `redis-connection-string` lesson), `SMTP`, `USER_ADMIN_PASSWORD`, - `CLIENT_SYSTEM_SECRET`, `SECRETS_ENCRYPTION_KEY`. -3. **Escape hatches** for the long tail (~40 env vars total exist): - `extraEnvVars`, `extraEnvVarsCM`, `extraEnvVarsSecret` per role, all - tpl-rendered. Plus `server.configuration` / `server.existingConfigmap` mounting - an `authup.server.core.conf` for the file-only options (middleware objects, - explicit CORS allowlist, per-field SMTP) — env still wins, so secrets stay in - env. - -Checksum annotations (`checksum/env`, `checksum/secret`, `checksum/provisioning`) -on both pod templates so config rotation rolls pods; opt-out flag. - -### 3.4 Secret management - -One chart-managed Secret (``) with the bitnami contract: - -```yaml -auth: - adminPassword: "" # "" => generated (lookup-stable across upgrades) - systemClientEnabled: false - systemClientSecret: "" # "" => generated when enabled - existingSecret: "" # whole-secret override - secretKeys: # key-name indirection for foreign secrets - adminPasswordKey: admin-password - systemClientSecretKey: system-client-secret - secretsEncryptionKeyKey: secrets-encryption-key -``` - -- **Generate-if-absent with upgrade stability**: explicit value → `lookup` of the - existing Secret → random. `helm.sh/resource-policy: keep` on the generated - Secret. Admin password and system-client secret are safe to generate (authup - only applies them at bootstrap unless `*_RESET=true`, which the chart exposes as - opt-in values, never hardcoded `"true"` like the PrivateAIM chart). -- **`SECRETS_ENCRYPTION_KEY` is special — never silently generated.** It is - effectively write-once (removing/rotating it while wrapped rows exist bricks MFA - seeds and wrapped signing keys). Modes: unset (default; authup warns at boot), - `auth.secretsEncryptionKey` explicit value, or `existingSecret` reference. A - generated mode is deliberately not offered in v1: `lookup` is inert under - `helm template` / ArgoCD, and a GitOps-driven regeneration of this particular - key is unrecoverable. NOTES.txt carries the back-up-your-key warning. -- The GitOps/lookup caveat is documented once, at the `auth` block. - -### 3.5 Database — the hybrid model - -```yaml -database: # selects what the app connects to - type: postgres # postgres | mysql -externalDatabase: # production path - host: "" - port: "" # "" => engine default (5432/3306) - user: authup - database: authup - password: "" - existingSecret: "" - existingSecretPasswordKey: password -postgresql: # built-in dev/small-prod instance - enabled: true # docker.io/library/postgres, single StatefulSet - auth: {username: authup, password: "", database: authup, existingSecret: ""} - persistence: {enabled: true, size: 8Gi, storageClass: ""} - image: {repository: postgres, tag: "17"} # docker-official, overridable -mysql: - enabled: false # docker.io/library/mysql, same shape -``` - -- **Dispatch helpers** (`authup.database.{type,host,port,name,user,secretName,passwordKey}`) - branch on `postgresql.enabled` / `mysql.enabled` / external — consuming - templates never know which mode is active (the keycloak `keycloak.database.*` - pattern). `DB_TYPE` follows the active built-in engine automatically; - `database.type` only matters for `externalDatabase`. -- **Built-in instances are vendored minimal templates, not subcharts**: one - single-instance StatefulSet + Service + Secret + PVC per engine, running - **docker-official images** (`postgres`, `mysql` — free, maintained, no bitnami - entanglement). Explicitly positioned as dev/small-prod convenience; README - points production at an external DB or an operator (CloudNativePG for postgres). - This is the cohort-converged posture: Authelia removed its DB subcharts, - authentik defaults `postgresql.enabled: false` and had to lobotomize the bitnami - chart to keep it; PrivateAIM's develop branch (PR #161, 2026-07-29) already - replaced bitnami postgresql with exactly this — a ~150-line vendored - StatefulSet on `postgres:17`. The post-bitnami landscape survey confirmed no - better dependency: groundhog2k is the only maintained non-bitnami chart family - covering both engines but is a bus-factor-1 project, and CNPG is an operator, - not a subchart. Owning ~150 lines per engine beats depending on any of that. - (Deliberate deviation from the cohort's `enabled: false` default: authup - cannot boot at all without a database, so `postgresql.enabled: true` keeps - `helm install` working out of the box; production values disable it.) -- **Validation**: template-time `fail` when neither a built-in engine is enabled - nor `externalDatabase.host` is set (boot cannot succeed — no sqlite on the - image); `fail` when both engines are enabled; `fail` on unknown `database.type`. -- The external-db password given inline still lands in a chart-managed Secret - (`-externaldb`), never in a pod env literal (bitnami's - `-externaldb` fallback-secret trick). -- DB auto-create at boot needs CREATE privilege; built-in instances pre-create the - database via the official images' `POSTGRES_DB` / `MYSQL_DATABASE` env, external - DBs get a documented note. - -### 3.6 Redis / cache - -```yaml -valkey: # built-in instance (BSD-licensed redis successor) - enabled: false - auth: {password: "", existingSecret: ""} -externalRedis: - url: "" # full redis:// URL, OR: - host: "", port: 6379, password: "", existingSecret: "" -``` - -- Composed into the single `REDIS` env var (URL form; stored in a Secret because - it embeds the password). The variable is `REDIS` — the docker-compose doc's - `REDIS_URL` does not exist in authup source. -- Default off (authup runs without it), **but**: `server.replicaCount > 1` or - server HPA enabled without redis configured is a hard template `fail` — with the - per-process MemoryCache fallback, the auth-code blob, token blocklist, and MFA - challenge nonces are per-pod, which breaks logins and MFA across replicas, not - just performance. NOTES also recommends redis whenever MFA is enabled (cache - availability is on the login-path SLO per authup docs). -- Built-in instance is valkey (docker-official `valkey/valkey`) — redis relicensed - in 2024; valkey is drop-in for authup's usage. - -### 3.7 Ingress and topology - -Two-host model as the default (server-core is the IdP origin serving the SSR auth -pages; client-admin-console is an ordinary RP; **cookie-domain sharing between the two is -unsupported by authup** — the chart never sets `NUXT_PUBLIC_COOKIE_DOMAIN` and -validates against foot-guns): - -```yaml -server: - publicUrl: "" # authoritative override; else derived from ingress - ingress: {enabled, hostname, path, pathType, ingressClassName, annotations, - tls, selfSigned, certManager, extraHosts, extraPaths, extraTls, extraRules} - route: {enabled, ...} # Gateway API HTTPRoute (52 cheap lines) -ui: - publicUrl: "" - ingress: {…same shape…} -``` - -**URL derivation is the chart's biggest UX win** (kills the #1 misconfiguration — -dead logins from a missing trusted origin, and the PrivateAIM `values_min.yaml` -publicUrl drift): - -- `PUBLIC_URL` ← `server.publicUrl` | derived `http(s)://` -- `NUXT_PUBLIC_API_URL` ← the same value (browser-reachable, never the cluster - Service DNS; the optional private `NUXT_API_URL` may point in-cluster for SSR) -- `NUXT_PUBLIC_PUBLIC_URL` ← `adminConsole.publicUrl` | derived from `adminConsole.ingress` -- `TRUSTED_ORIGINS` ← user list ∪ the UI origin (auto-appended unless disabled) -- `TRUST_PROXY` defaults to `"1"` (one ingress hop), not authup's spoofable - `true`-every-hop default. - -Sub-path single-host deployments are supported (authup rebases assets off -`publicUrl`'s pathname) and documented with the nginx rewrite + -`proxy-buffer-size` annotations PrivateAIM operationally converged on (16k+ -buffers for token-heavy responses). mTLS (`MTLS_PUBLIC_URL`, -`CERTIFICATE_SOURCE`) stays a documented escape hatch via `extraEnvVars`, not -first-class templating, in v1. - -### 3.8 Migrations and upgrades - -- server-core auto-migrates at boot (no flag to disable); a fresh install simply - boots. The startupProbe budget is generous (migrations + provisioning on first - boot). -- `server.migration.enabled` (default **false**) renders a **pre-upgrade-only** - hook Job (`args: ["server/core","migration","run"]`, same env helpers as the - Deployment so Job/Deployment can't drift). Not pre-install: on fresh installs - the database backing service isn't up before hooks run — the exact failure that - made authentik revert their migration Job. On upgrades the DB exists, and the - Job serializes DDL before new pods roll — recommended (and referenced by the - replicas>1 validation) for multi-replica deployments, since MySQL DDL is - non-transactional and concurrent boot migrations can race. -- `useHelmHooks: false` support: ArgoCD only. ArgoCD renders with - `helm template` and never runs Helm hooks, so it gets `argocd.argoproj.io` - annotations instead. Flux runs a real `helm upgrade` and honours Helm hooks, - so a plain Job there hits the immutable `spec.template` on the next upgrade. -- The hook Job sees only the PREVIOUS release's ConfigMaps and Secrets, so it - carries a narrowed env/mount set (DB_PASSWORD and the encryption key only, no - provisioning mount) plus a - hook-scoped copy of `authup.server.core.conf`, which `migration run` does - read. -- Value reshuffles get authentik-style tripwires: a `deprecations.yaml` template - fails loudly naming the moved key. BREAKING.md tracks migrations; chart - versioning is independent SemVer (0.major.minor pre-1.0), `appVersion` tracks - authup — never authentik's chart==app coupling (which strands chart fixes - between app releases). - -### 3.9 Provisioning files - -The PrivateAIM chart's one genuinely good block, kept and hardened: - -```yaml -server: - provisioning: - enabled: false - files: {} # filename -> content (tpl-rendered); extension picks the reader - existingConfigMap: "" # tpl-rendered -``` - -Mounted read-only at `/provisioning`; checksum-annotated -(the PrivateAIM version forgot this — edits never rolled pods); values doc states -fail-closed semantics (an invalid file aborts boot) and camelCase keys. - -### 3.10 Security posture - -- 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 `/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 - internally, and the values doc warns against routing `/metrics` through the - public ingress. -- NetworkPolicy shipped enabled-but-permissive (bitnami posture), instance-scoped - selectors, tightening opt-in. -- Default-derived secrets never render as pod env literals; `start123` never - appears anywhere (the chart generates instead). -- Anonymous `GET /` liveness endpoint for both services; terminationGracePeriodSeconds - default ≥ 30 (server-core has a 10s forced-exit teardown timer). - -### 3.11 Standard chrome (full battery, both roles) - -`replicaCount`, `resources` + `resourcesPreset`, `podAnnotations`/`podLabels`, -`nodeSelector`/`tolerations`/`affinity` (+ `podAntiAffinityPreset: soft`)/ -`topologySpreadConstraints`, `priorityClassName`, `schedulerName`, -`terminationGracePeriodSeconds`, `updateStrategy`, `revisionHistoryLimit`, -`command`/`args`/`lifecycleHooks` overrides, `customStartupProbe`/`customLivenessProbe`/ -`customReadinessProbe` + structured probe tuning, `initContainers`, `sidecars`, -`extraVolumes`/`extraVolumeMounts`, `extraPorts`, `service.{type,ports,nodePorts, -clusterIP,loadBalancerIP,annotations,sessionAffinity}`, `pdb`, `autoscaling.hpa`, -`networkPolicy`, `serviceAccount`, `metrics.serviceMonitor`; top-level -`commonLabels`, `commonAnnotations`, `extraDeploy`, `diagnosticMode`, -`global.{imageRegistry,imagePullSecrets,defaultStorageClass}`, `clusterDomain`, -`kubeVersion`/`apiVersions` overrides. Every passthrough tpl-rendered. - -### 3.12 Fail-fast validations (`validations.yaml`) - -Cross-field rules the JSON schema cannot express, one render-nothing template: - -1. No database configured / both built-in engines enabled / unknown `database.type`. -2. `server.replicaCount > 1` (or server HPA) without redis. -3. `mfa.required` without `mfa.enabled`; `loginThrottle` without event log - (mirrors authup's boot validations — fail at render, not at CrashLoopBackOff). -4. `auth.existingSecret` combined with inline passwords. -5. `adminConsole.enabled` with neither ingress nor explicit `adminConsole.publicUrl` when server - ingress is on (dead-login trap), and any config that would point the UI cookie - domain at the server host. -6. Tombstones for renamed values (grows over time). - -### 3.13 NOTES.txt - -Computed from the same helpers the manifests use: admin UI URL, OIDC -issuer/discovery URL, `kubectl get secret` one-liners for the generated -credentials, the TRUSTED_ORIGINS reminder when overridden, the -"PUBLIC_URL changes break enrolled WebAuthn credentials" warning, the -back-up-`SECRETS_ENCRYPTION_KEY` warning when set, and resource-less-deployment -warnings. - -## 4. Repo infrastructure - -### 4.1 Versioning & release - -- **release-please** (`release-type: helm`, component `authup`, - `include-v-in-tag: false` → tags `authup-x.y.z`) bumps `Chart.yaml` and - maintains CHANGELOG.md from conventional commits — the PrivateAIM-proven spine, - matching the monorepo's culture. -- **Publish on release**: `helm/chart-releaser-action` (`CR_SKIP_EXISTING`) → - GitHub Releases + gh-pages `index.yaml` (`https://helm.authup.org`, - CNAME-able later), then the 6-line loop `helm push … oci://ghcr.io/authup/helm-charts` - — dual classic + OCI, the authentik/authelia-converged shape. - `skip-github-release` on the chart component so release-please and - chart-releaser don't race. -- No chart signing initially (cohort-wide `sign: false`). -- Bootstrap chore: create the orphan `gh-pages` branch before the first release. - -### 4.2 CI (`lint-test.yaml`) - -1. `ct lint` (chart-dirs `charts`, `check-version-increment: false` — - release-please owns versions; yamllint config from authentik's `lintconf.yaml`; - yamale `chart_schema.yaml`). -2. helm-docs drift gate (pinned docker image, `git diff --exit-code`). -3. dadav/helm-schema drift gate (same pattern). Strict - `additionalProperties: false` schema **plus the values-coverage audit Authelia - lacks**: a grep-based CI check that every `.Values.*` referenced in templates - exists in values.yaml — strict schema + template drift silently disables - features (Authelia's HPA metrics are dead code because of exactly this). -4. `ah lint` (ArtifactHub metadata). -5. `ct install` on kind, gated by `ct list-changed`, uuid namespace, - `--timeout 600s`, over the `ci/*-values.yaml` matrix (postgres, mysql, - external-db + existingSecret fixture, valkey + 2 replicas, server-only). - Authelia's lint-only CI is why its containerPort drift and copy-paste HPA/PDB - bugs shipped; the install matrix is the cohort-proven safety net. -6. Non-blocking: hardcoded-image grep, kubeconform. - -All steps are `make` targets runnable locally (authelia's Makefile discipline). - -### 4.3 Dependency automation - -Renovate: `helm-values` manager for the built-in DB/valkey image tags, -github-actions manager for workflow pins, conventional commit messages so bumps -flow through release-please as patches. No `bumpVersion` (release-please owns the -chart version). A `repository_dispatch` hook from the authup monorepo's release -workflow opens the `appVersion` bump PR on app releases. - -## 5. Deliberate non-goals (with receipts) - -| Not doing | Because | +1. Provide Bitnami-grade values ergonomics without external chart dependencies: + existing-secret key mapping, tpl-rendered extension points, predictable + naming, diagnostics and standard workload controls. +2. Deploy every supported Authup v1.0.0-beta.64 role from the one upstream + `authup/authup` image. +3. Keep a default install useful: a combined Authup server plus built-in + PostgreSQL, while making external database and cache services the documented + production path. +4. Preserve a single browser origin in combined and split topologies. +5. Keep the repository solo-maintainable with generated documentation/schema, + executable render contracts and release-please-owned versions. + +The chart has no dependencies. The Bitnami licensing change, the retirement of +k8s-at-home/common, and Authelia's removal of database subcharts all make small +vendored backing-service templates cheaper and more reliable than another +library or database chart dependency. + +## 2. Runtime topology + +Authup beta.64 replaces app-path arguments with direct CLI roles: + +| Chart role | Args | Port | Default | +|---|---|---:|---| +| combined server | `start` | 3000 | yes | +| core API | `start core` | 3000 | split mode | +| auth console | `start console auth` | 3020 | split mode, required | +| admin console | `start console admin` | 3021 | split mode, optional | +| account console | `start console account` | 3022 | split mode, optional | +| worker | `start worker` | none | optional | +| migration Job | `migration run` | none | optional upgrades | + +`server.splitConsoles=false` keeps the minimum topology: one Deployment runs +the API, enabled consoles and in-process worker behavior. Setting it true +changes that Deployment to the core role and creates explicit console +Deployments. The auth console cannot be disabled in split mode because it owns +the login flow. Admin and account consoles can be scaled or disabled +independently. + +The worker is a separate Deployment only when `worker.enabled=true`. Its process +gets `WORKER_ENABLED=true`; the API gets `WORKER_ENABLED=false`. It shares +database, cache, configuration and log mechanics with core, but it has no +Service, ports or HTTP probes. It does not receive SMTP or bootstrap identity +secrets because those modules are outside the worker role. + +Each role has explicit templates. Their structural duplication is deliberate: +authentik built and later removed a generic role loop because heterogeneous +probes, ports, Services and settings became harder to understand. Shared +helpers are limited to genuinely identical env, mount, ingress and naming +mechanics. + +## 3. Configuration model + +Authup beta.64 reads one `authup.yml` schema across roles. The chart mounts an +inline or existing ConfigMap at `/etc/authup/authup.yml`. Environment variables +remain the primary interface and override file values. + +The chart exposes three layers: + +1. First-class values for load-bearing database, URL, feature, security and + role-ownership settings. +2. Secrets through `valueFrom.secretKeyRef`; Authup has no equivalent `*_FILE` + interface. +3. Tpl-rendered escape hatches (`server.config`, extra env carriers, volumes and + `server.configuration`) for the long tail. + +The chart does not mirror the complete application configuration schema. That +would require release-by-release maintenance like Authelia's large ConfigMap +template. `server.config` rejects names owned by first-class values, preventing +duplicate ConfigMap keys and hidden overrides. The small, stable theme manifest +is the one exception because composing it from structured values catches errors +that otherwise surface only during application boot. + +Filesystem locations follow the image contract: + +- configuration: `/etc/authup/authup.yml` +- provisioning: `/etc/authup/provisioning` +- file logs: `/var/log/authup` +- npm cache: `/tmp/.npm-cache` + +There is no chart-managed writable root and no `WRITABLE_DIRECTORY_PATH`. + +## 4. URLs and single-origin routing + +`server.publicUrl` is the deployment-wide public URL and OIDC issuer. When it +is empty, the chart derives it from server Ingress. Scheme checks run both on +literal values and after tpl rendering. + +In combined mode the one server exposes every enabled surface. Split mode must +preserve the same origin: + +- `/console/auth` routes to the auth console +- `/console/admin` routes to the admin console +- `/console/account` routes to the account console +- exact admin/account login start and callback paths route to core +- all remaining API paths route to core + +The console listeners serve from `/`, so the public prefix must be stripped. +Generated Kubernetes Ingress resources deliberately target ingress-nginx and +use its regex rewrite annotations. Gateway API HTTPRoutes use portable +`URLRewrite` filters with `ReplacePrefixMatch`. + +Each split console gets the shared `PUBLIC_URL` plus an `INTERNAL_URL` pointing +at the core Service for server-side calls. Database, Redis, SMTP and bootstrap +identity secrets never enter console pods. + +An empty HTTPRoute match defaults to a root catch-all. The chart therefore +fails a server route whose derived public URL contains a sub-path but supplies +no explicit match and rewrite. `route.enabled` supports boolean strings for +umbrella charts, and the strict helper validates those values even when the +corresponding role is disabled. + +## 5. Database and cache + +The published production image cannot use SQLite. Exactly one of these paths is +required: + +- built-in PostgreSQL (default) +- built-in MySQL +- `externalDatabase` + +The built-in services are small single-instance StatefulSets on official +images. They are appropriate for development and small deployments, not a +replacement for a production database operator or managed service. + +Database dispatch helpers hide the active engine from consuming templates and +always put passwords in Secrets. The chart never generates credentials for an +external database it does not own. + +Authup's cache variable is `REDIS`, a full connection URL. Because the URL can +contain a password, it is also Secret-backed. More than one API replica or an +API HPA requires Redis: the in-memory fallback makes authorization codes, +revocations and MFA challenges pod-local and breaks correctness. + +## 6. Secrets + +The chart-managed auth Secret uses explicit value, existing lookup, then random +generation for the initial admin password and optional system-client secret. +`helm.sh/resource-policy: keep` preserves it across uninstall/reinstall +mistakes. Pure template GitOps cannot make lookup-generated values stable, so +those users must provide explicit values or an existing Secret. + +`SECRETS_ENCRYPTION_KEY` is different: it is never generated. It is effectively +write-once because losing or rotating it makes wrapped MFA seeds and signing +keys unreadable. References are fail-closed and existing-secret use requires an +explicit enabled flag. + +## 7. Migrations and upgrades + +Authup core can initialize and migrate at startup. A pre-install migration Job +would run before chart-managed databases exist, so fresh installs rely on core +startup and its generous startup probe. + +`server.migration.enabled=true` creates a pre-upgrade Job. On upgrade, core gets +`MIGRATION_ENABLED=false` and the Job owns schema migration before pods roll. +This avoids concurrent DDL, especially with multiple API replicas and MySQL. + +Helm creates hooks before regular release resources. The Job therefore: + +- inlines the next release's non-secret config +- receives database password and optional encryption key only +- omits Redis, SMTP, bootstrap identity and provisioning inputs +- 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. + +## 8. Network policy + +Policies are opt-in and permissive by default. When tightened: + +- core ingress accepts the enabled split console components plus configured + ingress-controller selectors +- each console accepts HTTP ingress and can reach core +- worker egress permits DNS and release-local database/cache pods +- the hook-scoped migration policy permits DNS and release-local backing pods +- external services require operator-supplied `extraEgress` + +Component and instance labels scope every peer. The migration policy must remain +a hook; a regular policy would be created after the hook pod needs it, which is +the root cause of issue #22. + +## 9. Kubernetes resource conventions + +- Selectors contain only name, release instance and component. User labels do + not enter immutable selectors. +- Component fullnames truncate the base before suffixing with a suffix-specific + budget. The budget may tighten to make invalid names legal, but must not widen + and rename existing resources. A renamed kept Secret can rotate credentials. +- Checksums roll pods when any consumed chart-managed env, Secret, + configuration, provisioning or theme input changes. +- Tpl rendering applies to list/map extension points so umbrella charts can + inject their own values. +- The generated schema is strict except for intentionally extensible maps and + the `global` map. Helm copies the parent's full global map into subcharts, so + that node must stay open. + +## 10. Validation and testing + +Cross-field constraints live in `templates/validations.yaml`: database choice, +replica/cache requirements, security combinations, URL shape, Ingress hostname, +strict flags, moved-value tombstones and split-mode dependencies. + +`scripts/check-beta64-contract.py` renders manifests and asserts the upstream +integration boundary: args, ports, role env, secret isolation, mounts, +install/upgrade migration ownership, prefix rewrites, exact API routes, hook +annotations and NetworkPolicy peers. `make test` runs this alongside lint, every +CI values render and the values-coverage audit. CI additionally regenerates the +README/schema and installs the scenario matrix on kind. + +## 11. Releases + +release-please owns `Chart.yaml` version, both changelogs and the manifest. +`appVersion` tracks Authup independently. Breaking 0.x chart changes use a +Conventional Commit breaking marker and are recorded in `BREAKING.md` with a +fail-loud old-value guard when a key moves. + +Publishing uses hevi/chart-releaser to create the GitHub chart release, update +the classic repository index and push the OCI artifact. Built artifacts remain +idempotent on reruns. + +## 12. Deliberate non-goals + +| Not doing | Reason | |---|---| -| bitnami/common dependency or any library chart | Broadcom clampdown; k8s-at-home death killed authentik's v1; vendoring ~5 helpers is cheap | -| bitnami postgresql/mysql/redis subcharts | PrivateAIM migrating off; authentik lobotomized theirs; Authelia removed theirs | -| Config-file re-templating (Authelia's 714-line configMap) | authup is env-driven; the whole problem class disappears | -| Role-loop DRY deployments | authentik built it and reverted it | -| DaemonSet/StatefulSet kind-switch for the app | both services are stateless Deployments; Authelia is removing theirs | -| Traefik CRD middleware stack | forward-auth-specific; authup RPs integrate via OIDC | -| Pre-**install** migration Job | authentik reverted: hooks run before backing services exist; boot migration + startupProbe covers fresh installs | -| `authup` CLI supervisor as a pod | not routable via image entrypoint; anti-topology per authup docs | -| Chart version == appVersion | strands chart fixes between app releases (observed live on authentik main) | -| helm-unittest, signing, kubescape gates on day one | nobody in the cohort ships them; the kind matrix is the safety net | -| PVC for the writable dir | nothing durable lives there with an external DB (logs only); emptyDir | -| A "run without a database" demo mode | impossible on the published image (production forbids sqlite) | - -## 6. Post-critique amendments (applied) and deferred items - -A three-lens adversarial review (correctness / security / operator-UX) ran -against the initial scaffold; 40 verified findings were triaged. Applied, among -others: the release workflow's publish job runs unconditionally (chart-releaser -is idempotent via `CR_SKIP_EXISTING`; gating it on release-please outputs made -publishing unreachable under `skip-github-release`); ingress TLS secret names -derive from the component, not the hostname (wildcard hosts + shared-host -collisions); HTTPRoute hostnames strip ports; scheme-less URL values fail at -render instead of deriving a broken `"://"` origin into `TRUSTED_ORIGINS`; -`SECRETS_ENCRYPTION_KEY` from an existing secret requires an explicit -`auth.secretsEncryptionKeyEnabled` opt-in and the ref is never `optional` -(fail-closed KEK); external-db passwords are never generated (render-time fail -instead); checksum annotations cover the db/redis/smtp/valkey secrets; the -migration Job carries the full deployment env surface plus ArgoCD PreSync hook -annotations when `useHelmHooks=false`; component fullnames truncate before -suffixing; `server.config` collisions with first-class env names fail loudly; -the tightened UI NetworkPolicy always emits a peer; backing-store probes keep -credentials off argv; and the values-coverage audit resolves full dotted paths. - -Deferred deliberately (revisit before 1.0): per-credential `existingSecret` on -the built-in engines; NetworkPolicies for the backing stores; a non-root -default securityContext (blocked on testing the upstream root image under -uid 1000 in the kind matrix — DESIGN §3.10's hardened-default claim is -weakened to best-effort accordingly); an `ingress.blockMetrics` convenience; -`resourcesPreset`/`kubeVersion`/`apiVersions`/`clusterDomain` chrome (dropped -as dead values); the checksum-of-secret preimage caveat (cohort-standard -behavior, documented); and chart signing. - -**Publishing tool**: chart-releaser today; migration to hevi (tada5hi's own -versioner/releaser, already powering PrivateAIM/helm) is planned once -tada5hi/hevi#52 — the chart-releaser parity checklist (stable -`-` tags, idempotent skip-existing publish, custom-domain -index merge with CNAME preservation, release notes + OCI push) — is green. -This repo is hevi's designated second consumer. +| Bitnami/common or database subcharts | licensing and ecosystem churn outweigh a few local helpers/templates | +| Full Authup config-file templating | env-first configuration plus escape hatches avoid a schema treadmill | +| Generic role-loop templates | explicit heterogeneous roles are easier to audit and change | +| Pre-install migration Job | hooks run before chart-managed backing services exist | +| Service or HTTP probes for worker | the beta.64 worker has no HTTP listener | +| Different public origins for split consoles | Authup's browser and login contracts use one deployment-wide public URL | +| PVC for file logs | application state is in the database; log persistence belongs in collection infrastructure | +| Run-without-database demo mode | impossible in the production image | + +Deferred before chart 1.0: per-credential existing Secrets for built-in stores, +backing-store NetworkPolicies, a non-root default after upstream image support, +an ingress metrics-blocking convenience, and chart signing. diff --git a/charts/authup/README.md b/charts/authup/README.md index 0e22b72..2e8195f 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -7,18 +7,17 @@ # authup ![Version](https://img.shields.io/badge/Version-0.3.0?style=flat-square&color=informational) -![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: - -- **server-core** — the Authup IdP/API service: the OAuth2/OIDC protocol - surface, the server-rendered auth pages (login, consent, registration, - password recovery) and the `/account` self-service console - (`server.features.accountConsole`). This is the identity origin. -- **client-admin-console** — the Nuxt-based admin UI, an ordinary OAuth2 relying party - (optional; disable with `adminConsole.enabled=false` for a headless IdP). -- optionally, single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in - instances on docker-official images — a convenience for dev and small +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.64](https://img.shields.io/badge/AppVersion-1.0.0--beta.64-informational?style=flat-square) + +Authup is an authentication & authorization system. This chart deploys its combined or split API, console and worker roles, with optional built-in PostgreSQL, MySQL and Valkey instances. It runs Authup v1.0.0-beta.64's role-based +CLI topology: + +- one combined API and console workload by default (`start`) +- optional split API (`start core`) plus auth, admin and account console + workloads (`start console `), enabled with `server.splitConsoles=true` +- an optional dedicated background worker (`start worker`) +- optional single-instance **PostgreSQL**, **MySQL** or **Valkey** built-in + instances on docker-official images, a convenience for dev and small deployments, not the production database story. > This chart is pre-1.0: breaking changes land on the middle version digit and @@ -34,8 +33,8 @@ helm install authup authup/authup helm install authup oci://ghcr.io/authup/helm/authup ``` -The default install brings up server-core, the admin UI and a built-in -PostgreSQL. Retrieve the generated admin password: +The default install starts the combined server and a built-in PostgreSQL. +Retrieve the generated admin password: ```bash kubectl get secret authup -o jsonpath='{.data.admin-password}' | base64 -d @@ -45,15 +44,16 @@ kubectl get secret authup -o jsonpath='{.data.admin-password}' | base64 -d ```yaml server: + publicUrl: https://auth.example.com ingress: enabled: true hostname: auth.example.com tls: true -ui: - ingress: + migration: enabled: true - hostname: authup.example.com - tls: true + +worker: + enabled: true postgresql: enabled: false @@ -70,9 +70,44 @@ auth: existingSecret: my-authup-secret # admin-password (+ optional system-client-secret, secrets-encryption-key) ``` -`PUBLIC_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_PUBLIC_URL` and -`TRUSTED_ORIGINS` are derived from the two ingress hostnames automatically — -the UI origin is appended to the trusted origins so logins work out of the box. +`PUBLIC_URL` is derived from `server.ingress` when it is not set directly. +The combined server, split consoles and API must share this public origin. +Split Ingress resources use ingress-nginx rewrites for `/console/auth`, +`/console/admin` and `/console/account`; Gateway API HTTPRoutes express the +same topology with portable `URLRewrite` filters. + +To scale or isolate roles independently while keeping one browser origin: + +```yaml +server: + publicUrl: https://auth.example.com + splitConsoles: true + ingress: + enabled: true + hostname: auth.example.com + tls: true + +authConsole: + ingress: + enabled: true + hostname: auth.example.com + tls: true +adminConsole: + enabled: true + ingress: + enabled: true + hostname: auth.example.com + tls: true +accountConsole: + enabled: true + ingress: + enabled: true + hostname: auth.example.com + tls: true +``` + +The split auth console is required because it owns the login flow. Disable the +admin or account console independently when those surfaces are not needed. Notable operational facts (enforced or warned about by the chart): @@ -84,6 +119,10 @@ Notable operational facts (enforced or warned about by the chart): credentials and the OIDC issuer. - **`auth.secretsEncryptionKey` is write-once.** The chart never generates it; set it deliberately and back it up. +- **Configuration is mounted at `/etc/authup/authup.yml`.** Provisioning files + live under `/etc/authup/provisioning`; file logs use `/var/log/authup`. +- **The migration Job is upgrade-only.** Fresh installs let the server create + its database after built-in database resources become ready. - The long tail of Authup options is available via `server.config` (plain env name/value pairs), `server.extraEnvVars`, or a mounted `server.configuration` file. See the @@ -160,8 +199,103 @@ Kubernetes: `>=1.25.0-0` | Key | Type | Default | Description | |-----|------|---------|-------------| +| accountConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | +| accountConsole.args | list | `[]` | Override the container args | +| accountConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | +| accountConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | +| accountConsole.autoscaling.hpa.minReplicas | int | `2` | Minimum replicas | +| accountConsole.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | +| accountConsole.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | +| accountConsole.command | list | `[]` | Override the container command | +| accountConsole.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap | +| accountConsole.containerPorts.http | int | `3022` | Account console listener port | +| accountConsole.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context (same root-image caveat as the server) | +| accountConsole.customLivenessProbe | object | `{}` | Custom liveness probe | +| accountConsole.customReadinessProbe | object | `{}` | Custom readiness probe | +| accountConsole.customStartupProbe | object | `{}` | Custom startup probe | +| accountConsole.disableRestartOnChanges | bool | `false` | Disable the checksum annotations that roll pods on config changes | +| accountConsole.enabled | bool | `true` | Enable the account console and deploy it separately in split-console mode | +| accountConsole.extraEnvVars | list | `[]` | Extra environment variables for the account console container | +| accountConsole.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | +| accountConsole.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | +| accountConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | +| accountConsole.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | +| accountConsole.hostAliases | list | `[]` | Pod host aliases | +| accountConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | +| accountConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | +| accountConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split account console | +| accountConsole.ingress.extraHosts | list | `[]` | Extra hosts | +| accountConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | +| accountConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | +| accountConsole.ingress.extraTls | list | `[]` | Extra TLS entries | +| accountConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | +| accountConsole.ingress.ingressClassName | string | `""` | Ingress class name | +| accountConsole.ingress.path | string | `"/console/account"` | Public console path (the generated ingress strips it) | +| accountConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | +| accountConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | +| accountConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | +| accountConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | +| accountConsole.livenessProbe.enabled | bool | `true` | Enable the liveness probe | +| accountConsole.livenessProbe.failureThreshold | int | `3` | | +| accountConsole.livenessProbe.initialDelaySeconds | int | `0` | | +| accountConsole.livenessProbe.periodSeconds | int | `30` | | +| accountConsole.livenessProbe.successThreshold | int | `1` | | +| accountConsole.livenessProbe.timeoutSeconds | int | `5` | | +| accountConsole.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere | +| accountConsole.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | +| accountConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the UI | +| accountConsole.networkPolicy.extraEgress | list | `[]` | Extra egress rules | +| accountConsole.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | +| accountConsole.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | +| accountConsole.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | +| accountConsole.nodeSelector | object | `{}` | Node selector | +| accountConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the UI | +| accountConsole.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | +| accountConsole.pdb.minAvailable | string | `""` | Minimum available pods | +| accountConsole.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | +| accountConsole.podAntiAffinityPreset | string | `"soft"` | Pod anti-affinity preset: soft, hard or "" | +| accountConsole.podLabels | object | `{}` | Pod labels (tpl-rendered) | +| accountConsole.podSecurityContext | object | `{"enabled":true,"fsGroup":1000}` | Pod security context | +| accountConsole.priorityClassName | string | `""` | Priority class name | +| accountConsole.readinessProbe.enabled | bool | `true` | Enable the readiness probe | +| accountConsole.readinessProbe.failureThreshold | int | `3` | | +| accountConsole.readinessProbe.initialDelaySeconds | int | `0` | | +| accountConsole.readinessProbe.periodSeconds | int | `10` | | +| accountConsole.readinessProbe.successThreshold | int | `1` | | +| accountConsole.readinessProbe.timeoutSeconds | int | `5` | | +| accountConsole.replicaCount | int | `1` | Number of account console replicas | +| accountConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Account console container resources | +| accountConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | +| accountConsole.route.annotations | object | `{}` | HTTPRoute annotations | +| accountConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the account console (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) | +| accountConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | +| accountConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | +| accountConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | +| accountConsole.route.parentRefs | list | `[]` | Gateway parentRefs | +| accountConsole.schedulerName | string | `""` | Scheduler name | +| accountConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) | +| accountConsole.service.clusterIP | string | `""` | Static cluster IP | +| accountConsole.service.externalTrafficPolicy | string | `"Cluster"` | External traffic policy | +| accountConsole.service.extraPorts | list | `[]` | Extra service ports | +| accountConsole.service.loadBalancerIP | string | `""` | LoadBalancer IP | +| accountConsole.service.loadBalancerSourceRanges | list | `[]` | LoadBalancer source ranges | +| accountConsole.service.nodePorts.http | string | `""` | Node port ("" = auto-assign) | +| accountConsole.service.ports.http | int | `3022` | Service HTTP port | +| accountConsole.service.sessionAffinity | string | `"None"` | Session affinity | +| accountConsole.service.sessionAffinityConfig | object | `{}` | Session affinity config | +| accountConsole.service.type | string | `"ClusterIP"` | Service type | +| accountConsole.sidecars | list | `[]` | Sidecar containers (tpl-rendered) | +| accountConsole.startupProbe.enabled | bool | `true` | Enable the startup probe | +| accountConsole.startupProbe.failureThreshold | int | `24` | | +| accountConsole.startupProbe.initialDelaySeconds | int | `5` | | +| accountConsole.startupProbe.periodSeconds | int | `5` | | +| accountConsole.startupProbe.successThreshold | int | `1` | | +| accountConsole.startupProbe.timeoutSeconds | int | `5` | | +| accountConsole.terminationGracePeriodSeconds | int | `30` | Pod termination grace period | +| accountConsole.tolerations | list | `[]` | Tolerations | +| accountConsole.topologySpreadConstraints | list | `[]` | Topology spread constraints | +| accountConsole.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | | adminConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | -| adminConsole.apiUrl | string | `""` | Browser-facing server-core URL (NUXT_PUBLIC_API_URL). "" = the server public URL. Must be reachable from the user's browser, never a cluster-internal DNS name | | adminConsole.args | list | `[]` | Override the container args | | adminConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | | adminConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | @@ -170,13 +304,14 @@ Kubernetes: `>=1.25.0-0` | adminConsole.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | | adminConsole.command | list | `[]` | Override the container command | | adminConsole.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap | +| adminConsole.containerPorts.http | int | `3021` | Admin console listener port | | adminConsole.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context (same root-image caveat as the server) | | adminConsole.customLivenessProbe | object | `{}` | Custom liveness probe | | adminConsole.customReadinessProbe | object | `{}` | Custom readiness probe | | adminConsole.customStartupProbe | object | `{}` | Custom startup probe | | adminConsole.disableRestartOnChanges | bool | `false` | Disable the checksum annotations that roll pods on config changes | -| adminConsole.enabled | bool | `true` | Deploy the client-admin-console admin UI (false = headless IdP) | -| adminConsole.extraEnvVars | list | `[]` | Extra environment variables for the UI container | +| adminConsole.enabled | bool | `true` | Enable the admin console and deploy it separately in split-console mode | +| adminConsole.extraEnvVars | list | `[]` | Extra environment variables for the admin console container | | adminConsole.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | | adminConsole.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | | adminConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | @@ -184,18 +319,17 @@ Kubernetes: `>=1.25.0-0` | adminConsole.hostAliases | list | `[]` | Pod host aliases | | adminConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | | adminConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | -| adminConsole.ingress.enabled | bool | `false` | Enable ingress for the UI | +| adminConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split admin console | | adminConsole.ingress.extraHosts | list | `[]` | Extra hosts | | adminConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | adminConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | adminConsole.ingress.extraTls | list | `[]` | Extra TLS entries | | adminConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | | adminConsole.ingress.ingressClassName | string | `""` | Ingress class name | -| adminConsole.ingress.path | string | `"/"` | Ingress path | +| adminConsole.ingress.path | string | `"/console/admin"` | Public console path (the generated ingress strips it) | | adminConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | | adminConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | | adminConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | -| adminConsole.internalApiUrl | string | `""` | Server-side (SSR) API URL override (NUXT_API_URL), e.g. the in-cluster service URL to keep SSR traffic off the ingress | | adminConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | | adminConsole.livenessProbe.enabled | bool | `true` | Enable the liveness probe | | adminConsole.livenessProbe.failureThreshold | int | `3` | | @@ -219,20 +353,19 @@ Kubernetes: `>=1.25.0-0` | adminConsole.podLabels | object | `{}` | Pod labels (tpl-rendered) | | adminConsole.podSecurityContext | object | `{"enabled":true,"fsGroup":1000}` | Pod security context | | adminConsole.priorityClassName | string | `""` | Priority class name | -| adminConsole.publicUrl | string | `""` | Public URL of the UI (NUXT_PUBLIC_PUBLIC_URL). "" = derived from adminConsole.ingress | | adminConsole.readinessProbe.enabled | bool | `true` | Enable the readiness probe | | adminConsole.readinessProbe.failureThreshold | int | `3` | | | adminConsole.readinessProbe.initialDelaySeconds | int | `0` | | | adminConsole.readinessProbe.periodSeconds | int | `10` | | | adminConsole.readinessProbe.successThreshold | int | `1` | | | adminConsole.readinessProbe.timeoutSeconds | int | `5` | | -| adminConsole.replicaCount | int | `1` | Number of UI replicas (fully stateless, scale freely) | -| adminConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | UI container resources | +| adminConsole.replicaCount | int | `1` | Number of admin console replicas | +| adminConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Admin console 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; "false" and "" disable it, anything else fails the render) | +| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the admin console (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.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | | adminConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | | adminConsole.route.parentRefs | list | `[]` | Gateway parentRefs | | adminConsole.schedulerName | string | `""` | Scheduler name | @@ -243,7 +376,7 @@ Kubernetes: `>=1.25.0-0` | adminConsole.service.loadBalancerIP | string | `""` | LoadBalancer IP | | adminConsole.service.loadBalancerSourceRanges | list | `[]` | LoadBalancer source ranges | | adminConsole.service.nodePorts.http | string | `""` | Node port ("" = auto-assign) | -| adminConsole.service.ports.http | int | `3000` | Service HTTP port (the container port is fixed at 3000) | +| adminConsole.service.ports.http | int | `3021` | Service HTTP port | | adminConsole.service.sessionAffinity | string | `"None"` | Session affinity | | adminConsole.service.sessionAffinityConfig | object | `{}` | Session affinity config | | adminConsole.service.type | string | `"ClusterIP"` | Service type | @@ -269,6 +402,102 @@ Kubernetes: `>=1.25.0-0` | auth.systemClientEnabled | bool | `false` | Provision the built-in system client (CLIENT_SYSTEM_ENABLED); required for machine-to-machine consumers | | auth.systemClientSecret | string | `""` | System client secret ("" = generate once when systemClientEnabled) | | auth.systemClientSecretReset | bool | `false` | Re-assert the system client secret on every boot (CLIENT_SYSTEM_SECRET_RESET) | +| authConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | +| authConsole.args | list | `[]` | Override the container args | +| authConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | +| authConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | +| authConsole.autoscaling.hpa.minReplicas | int | `2` | Minimum replicas | +| authConsole.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | +| authConsole.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | +| authConsole.command | list | `[]` | Override the container command | +| authConsole.config | object | `{}` | Extra environment variables rendered literally into the env ConfigMap | +| authConsole.containerPorts.http | int | `3020` | Auth console listener port | +| authConsole.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context (same root-image caveat as the server) | +| authConsole.customLivenessProbe | object | `{}` | Custom liveness probe | +| authConsole.customReadinessProbe | object | `{}` | Custom readiness probe | +| authConsole.customStartupProbe | object | `{}` | Custom startup probe | +| authConsole.disableRestartOnChanges | bool | `false` | Disable the checksum annotations that roll pods on config changes | +| authConsole.enabled | bool | `true` | Enable the auth console and deploy it separately in split-console mode | +| authConsole.extraEnvVars | list | `[]` | Extra environment variables for the auth console container | +| authConsole.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | +| authConsole.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | +| authConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | +| authConsole.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | +| authConsole.hostAliases | list | `[]` | Pod host aliases | +| authConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | +| authConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | +| authConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split auth console | +| authConsole.ingress.extraHosts | list | `[]` | Extra hosts | +| authConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | +| authConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | +| authConsole.ingress.extraTls | list | `[]` | Extra TLS entries | +| authConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | +| authConsole.ingress.ingressClassName | string | `""` | Ingress class name | +| authConsole.ingress.path | string | `"/console/auth"` | Public console path (the generated ingress strips it) | +| authConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | +| authConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | +| authConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | +| authConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | +| authConsole.livenessProbe.enabled | bool | `true` | Enable the liveness probe | +| authConsole.livenessProbe.failureThreshold | int | `3` | | +| authConsole.livenessProbe.initialDelaySeconds | int | `0` | | +| authConsole.livenessProbe.periodSeconds | int | `30` | | +| authConsole.livenessProbe.successThreshold | int | `1` | | +| authConsole.livenessProbe.timeoutSeconds | int | `5` | | +| authConsole.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere | +| authConsole.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | +| authConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the UI | +| authConsole.networkPolicy.extraEgress | list | `[]` | Extra egress rules | +| authConsole.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | +| authConsole.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | +| authConsole.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | +| authConsole.nodeSelector | object | `{}` | Node selector | +| authConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the UI | +| authConsole.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | +| authConsole.pdb.minAvailable | string | `""` | Minimum available pods | +| authConsole.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | +| authConsole.podAntiAffinityPreset | string | `"soft"` | Pod anti-affinity preset: soft, hard or "" | +| authConsole.podLabels | object | `{}` | Pod labels (tpl-rendered) | +| authConsole.podSecurityContext | object | `{"enabled":true,"fsGroup":1000}` | Pod security context | +| authConsole.priorityClassName | string | `""` | Priority class name | +| authConsole.readinessProbe.enabled | bool | `true` | Enable the readiness probe | +| authConsole.readinessProbe.failureThreshold | int | `3` | | +| authConsole.readinessProbe.initialDelaySeconds | int | `0` | | +| authConsole.readinessProbe.periodSeconds | int | `10` | | +| authConsole.readinessProbe.successThreshold | int | `1` | | +| authConsole.readinessProbe.timeoutSeconds | int | `5` | | +| authConsole.replicaCount | int | `1` | Number of auth console replicas | +| authConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Auth console container resources | +| authConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | +| authConsole.route.annotations | object | `{}` | HTTPRoute annotations | +| authConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the auth console (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) | +| authConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | +| authConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | +| authConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | +| authConsole.route.parentRefs | list | `[]` | Gateway parentRefs | +| authConsole.schedulerName | string | `""` | Scheduler name | +| authConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) | +| authConsole.service.clusterIP | string | `""` | Static cluster IP | +| authConsole.service.externalTrafficPolicy | string | `"Cluster"` | External traffic policy | +| authConsole.service.extraPorts | list | `[]` | Extra service ports | +| authConsole.service.loadBalancerIP | string | `""` | LoadBalancer IP | +| authConsole.service.loadBalancerSourceRanges | list | `[]` | LoadBalancer source ranges | +| authConsole.service.nodePorts.http | string | `""` | Node port ("" = auto-assign) | +| authConsole.service.ports.http | int | `3020` | Service HTTP port | +| authConsole.service.sessionAffinity | string | `"None"` | Session affinity | +| authConsole.service.sessionAffinityConfig | object | `{}` | Session affinity config | +| authConsole.service.type | string | `"ClusterIP"` | Service type | +| authConsole.sidecars | list | `[]` | Sidecar containers (tpl-rendered) | +| authConsole.startupProbe.enabled | bool | `true` | Enable the startup probe | +| authConsole.startupProbe.failureThreshold | int | `24` | | +| authConsole.startupProbe.initialDelaySeconds | int | `5` | | +| authConsole.startupProbe.periodSeconds | int | `5` | | +| authConsole.startupProbe.successThreshold | int | `1` | | +| authConsole.startupProbe.timeoutSeconds | int | `5` | | +| authConsole.terminationGracePeriodSeconds | int | `30` | Pod termination grace period | +| authConsole.tolerations | list | `[]` | Tolerations | +| authConsole.topologySpreadConstraints | list | `[]` | Topology spread constraints | +| authConsole.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | | commonAnnotations | object | `{}` | Annotations added to every object | | commonLabels | object | `{}` | Labels added to every object | | database.type | string | `"postgres"` | Database engine when using externalDatabase: postgres or mysql | @@ -297,7 +526,7 @@ Kubernetes: `>=1.25.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Authup image pull policy | | image.pullSecrets | list | `[]` | Authup image pull secrets | | image.registry | string | `"docker.io"` | Authup image registry | -| image.repository | string | `"authup/authup"` | Authup image repository (one image serves both services) | +| image.repository | string | `"authup/authup"` | Authup image repository (one image serves every application role) | | image.tag | string | `""` | Authup image tag (defaults to the chart appVersion) | | mysql.affinity | object | `{}` | MySQL affinity | | mysql.auth.database | string | `"authup"` | MySQL database name (created on first boot) | @@ -348,20 +577,20 @@ Kubernetes: `>=1.25.0-0` | server.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | | 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 /var/lib/authup and /tmp to keep readOnlyRootFilesystem viable. | +| server.configuration | string | `""` | Content of authup.yml mounted at /etc/authup/authup.yml 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 writable log and npm cache paths; the chart mounts emptyDirs at /var/log/authup and /tmp. | | server.customLivenessProbe | object | `{}` | Custom liveness probe | | server.customReadinessProbe | object | `{}` | Custom readiness probe | | server.customStartupProbe | object | `{}` | Custom startup probe overriding the structured one | | server.disableRestartOnChanges | bool | `false` | Disable the checksum annotations that roll pods on config/secret changes | | server.enabled | bool | `true` | Deploy the server-core service | -| server.existingConfigmap | string | `""` | Existing ConfigMap holding authup.server.core.conf (tpl-rendered) | +| server.existingConfigmap | string | `""` | Existing ConfigMap holding authup.yml (tpl-rendered) | | server.extraEnvVars | list | `[]` | Extra environment variables for the server container | | server.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | | server.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | | server.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | | server.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | -| server.features.accountConsole | bool | `true` | Serve the account self-service console at /account (profile, password, authenticators, sessions, applications). ACCOUNT_CONSOLE_ENABLED; disable it when you run your own portal | +| server.features.accountConsole | string | `""` | Deprecated: use accountConsole.enabled. Any non-empty value fails. | | server.features.emailVerification | bool | `false` | Enable email verification (EMAIL_VERIFICATION_ENABLED; requires SMTP) | | server.features.passwordRecovery | bool | `false` | Enable password recovery (PASSWORD_RECOVERY_ENABLED; requires SMTP) | | server.features.registration | bool | `false` | Enable self-service user registration (REGISTRATION_ENABLED) | @@ -399,7 +628,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 `server/core migration run` as a pre-upgrade hook Job. Recommended for multi-replica deployments (serializes DDL before pods roll). Fresh installs migrate at boot regardless. | +| 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 migrate at boot regardless. | | 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) | @@ -423,7 +652,7 @@ Kubernetes: `>=1.25.0-0` | server.provisioning.existingConfigMap | string | `""` | Existing ConfigMap with provisioning files (tpl-rendered) | | server.provisioning.existingSecret | string | `""` | Existing Secret with provisioning files (tpl-rendered; takes precedence — use for provisioning content that carries credentials) | | server.provisioning.files | object | `{}` | Map of filename -> file content (tpl-rendered; the extension selects the reader). Lands in a ConfigMap — credential-bearing provisioning content belongs in existingSecret instead | -| server.publicUrl | string | `""` | Public URL of server-core (PUBLIC_URL) — the OIDC issuer origin. "" = derived from server.ingress when enabled. Changing it later breaks enrolled WebAuthn credentials and the OIDC issuer. | +| server.publicUrl | string | `""` | Public URL of Authup (PUBLIC_URL), shared by the API and consoles and used as the OIDC issuer origin. "" = derived from server.ingress when enabled. Changing it later breaks enrolled WebAuthn credentials and the OIDC issuer. | | server.readinessProbe.enabled | bool | `true` | Enable the readiness probe (GET / status endpoint) | | server.readinessProbe.failureThreshold | int | `3` | | | server.readinessProbe.initialDelaySeconds | int | `0` | | @@ -452,6 +681,7 @@ Kubernetes: `>=1.25.0-0` | server.service.sessionAffinityConfig | object | `{}` | Session affinity config | | server.service.type | string | `"ClusterIP"` | Service type | | server.sidecars | list | `[]` | Sidecar containers (tpl-rendered) | +| server.splitConsoles | bool | `false` | Run API-only server pods and deploy the console workloads separately | | server.startupProbe.enabled | bool | `true` | Enable the startup probe (first boot runs database creation, migrations and provisioning) | | server.startupProbe.failureThreshold | int | `60` | | | server.startupProbe.initialDelaySeconds | int | `5` | | @@ -475,11 +705,10 @@ Kubernetes: `>=1.25.0-0` | 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 | | server.trustedOrigins | list | `[]` | Additional trusted first-party app origins (TRUSTED_ORIGINS). Each entry is added to the redirect allowlist of the per-realm built-in system clients (admin-console, account-console), so any listed origin can complete a login and obtain a full-permission token. A host may carry a single "*" (https://*.example.com); "**" in a host is rejected by authup at boot. List or comma-separated string; tpl-rendered. | -| server.trustedOriginsAppendAdminConsole | bool | `true` | Automatically append the client-admin-console UI origin to TRUSTED_ORIGINS (removes the most common dead-login misconfiguration) | | server.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | | serviceAccount.annotations | object | `{}` | ServiceAccount annotations (tpl-rendered) | | serviceAccount.automountServiceAccountToken | bool | `false` | Automount the service account token | -| serviceAccount.create | bool | `true` | Create a ServiceAccount (shared by both services) | +| serviceAccount.create | bool | `true` | Create a ServiceAccount (shared by all application roles) | | serviceAccount.name | string | `""` | ServiceAccount name ("" = generated from the fullname) | | 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) | @@ -500,3 +729,43 @@ Kubernetes: `>=1.25.0-0` | valkey.podSecurityContext | object | `{"enabled":true,"fsGroup":999}` | Valkey pod security context | | valkey.resources | object | `{"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}}` | Valkey container resources | | valkey.tolerations | list | `[]` | Valkey tolerations | +| worker.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | +| worker.args | list | `[]` | Override the container args | +| worker.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the worker | +| worker.autoscaling.hpa.maxReplicas | int | `3` | Maximum replicas | +| worker.autoscaling.hpa.minReplicas | int | `1` | Minimum replicas | +| worker.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | +| worker.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | +| worker.command | list | `[]` | Override the container command | +| worker.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context | +| worker.disableRestartOnChanges | bool | `false` | Disable checksum annotations that roll pods on configuration changes | +| worker.enabled | bool | `false` | Deploy a dedicated background worker | +| worker.extraEnvVars | list | `[]` | Extra environment variables for the worker container | +| worker.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | +| worker.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | +| worker.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | +| worker.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | +| worker.hostAliases | list | `[]` | Pod host aliases | +| worker.initContainers | list | `[]` | Init containers (tpl-rendered) | +| worker.lifecycleHooks | object | `{}` | Container lifecycle hooks | +| worker.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | +| worker.networkPolicy.enabled | bool | `false` | Create an egress NetworkPolicy for the worker | +| worker.networkPolicy.extraEgress | list | `[]` | Extra egress rules | +| worker.nodeSelector | object | `{}` | Node selector | +| worker.pdb.create | bool | `false` | Create a PodDisruptionBudget for the worker | +| worker.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | +| worker.pdb.minAvailable | string | `""` | Minimum available pods | +| worker.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | +| worker.podAntiAffinityPreset | string | `"soft"` | Pod anti-affinity preset: soft, hard or "" | +| worker.podLabels | object | `{}` | Pod labels (tpl-rendered) | +| worker.podSecurityContext | object | `{"enabled":true,"fsGroup":1000}` | Pod security context | +| worker.priorityClassName | string | `""` | Priority class name | +| worker.replicaCount | int | `1` | Number of worker replicas (one is normally sufficient) | +| worker.resources | object | `{"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Worker container resources | +| worker.revisionHistoryLimit | int | `3` | Deployment revision history limit | +| worker.schedulerName | string | `""` | Scheduler name | +| worker.sidecars | list | `[]` | Sidecar containers (tpl-rendered) | +| worker.terminationGracePeriodSeconds | int | `30` | Pod termination grace period | +| worker.tolerations | list | `[]` | Tolerations | +| worker.topologySpreadConstraints | list | `[]` | Topology spread constraints | +| worker.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 944aa95..764ec7a 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -3540,7 +3540,7 @@ }, "repository": { "default": "authup/authup", - "description": "Authup image repository (one image serves both services)", + "description": "Authup image repository (one image serves every application role)", "required": [], "title": "repository", "type": "string" @@ -5675,7 +5675,7 @@ }, "create": { "default": true, - "description": "Create a ServiceAccount (shared by both services)", + "description": "Create a ServiceAccount (shared by all application roles)", "required": [], "title": "create", "type": "boolean" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 94375c3..95e697e 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -54,7 +54,7 @@ diagnosticMode: image: # -- Authup image registry registry: docker.io - # -- Authup image repository (one image serves both services) + # -- Authup image repository (one image serves every application role) repository: authup/authup # -- Authup image tag (defaults to the chart appVersion) tag: "" @@ -1819,7 +1819,7 @@ worker: ## @section Service account serviceAccount: - # -- Create a ServiceAccount (shared by both services) + # -- Create a ServiceAccount (shared by all application roles) create: true # -- ServiceAccount name ("" = generated from the fullname) name: "" From cecdbfbc2f741129313f5fbd17c9e6815a0fe184 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 4 Sep 2026 21:21:40 +0200 Subject: [PATCH 08/14] fix(authup): keep split console routes aligned with core --- .agents/architecture.md | 6 +- .agents/testing.md | 3 +- .github/workflows/lint-test.yaml | 5 + AGENTS.md | 3 +- CONTRIBUTING.md | 3 +- DESIGN.md | 6 + charts/authup/BREAKING.md | 3 +- charts/authup/README.md | 44 ++---- charts/authup/README.md.gotmpl | 10 +- charts/authup/ci/split-values.yaml | 3 - charts/authup/templates/NOTES.txt | 6 +- .../templates/account-console/httproute.yaml | 8 -- .../templates/account-console/ingress.yaml | 2 +- .../templates/admin-console/httproute.yaml | 8 -- .../templates/admin-console/ingress.yaml | 2 +- .../templates/auth-console/httproute.yaml | 8 -- .../templates/auth-console/ingress.yaml | 2 +- charts/authup/templates/validations.yaml | 28 +++- charts/authup/values.schema.json | 136 ++---------------- charts/authup/values.yaml | 50 ++----- scripts/check-beta64-contract.py | 52 +++++++ 21 files changed, 150 insertions(+), 238 deletions(-) diff --git a/.agents/architecture.md b/.agents/architecture.md index 36d4ba6..6856286 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -20,7 +20,8 @@ operational invariants that template changes must preserve. 4. **Worker ownership is explicit.** `worker.enabled=true` sets `WORKER_ENABLED=true` on the worker and `WORKER_ENABLED=false` on the server. The worker gets database and Redis credentials, but not SMTP, bootstrap - identity secrets, migrations, or console secrets. + identity secrets, migrations, or console secrets. It requires the server + because both roles share chart-managed configuration and credentials. 5. **The filesystem contract is fixed.** Configuration is `authup.yml` at `/etc/authup/authup.yml`, provisioning is `/etc/authup/provisioning`, and logs are `/var/log/authup`. There is no chart-managed writable root and no @@ -67,6 +68,9 @@ operational invariants that template changes must preserve. resources use ingress-nginx regex rewrites. Gateway API routes use `URLRewrite` with `ReplacePrefixMatch`. Exact admin/account login and callback paths must remain on the API before broader console prefixes. + A generated console Ingress or HTTPRoute requires the matching server + resource so those core-owned paths cannot disappear. Split mode rejects a + path-prefixed server public URL. 16. **HTTPRoute flags are strict.** `route.enabled` accepts a boolean or a template-rendered boolean string. `authup.flag` validates every role even when that role is disabled, because a non-empty string `"false"` is truthy diff --git a/.agents/testing.md b/.agents/testing.md index 8ba1a82..8c2719c 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -43,7 +43,8 @@ and cache, plus restrictive NetworkPolicies. - both inline and existing configuration ConfigMaps - invalid route flags, including flags on disabled roles - an HTTPRoute catch-all created accidentally from a sub-path public URL -- `server.splitConsoles=true` without the server or auth console +- `server.splitConsoles=true` without the server or auth console, and a worker + enabled without its shared server configuration - non-empty `server.features.accountConsole`, which moved to `accountConsole.enabled` - invalid theme manifests or dangerous trusted-origin globstars diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index 7ef39fa..f9fbf95 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -47,6 +47,11 @@ jobs: - name: Check values coverage run: make lint-values-coverage + - name: Check Authup beta.64 contract + run: | + python3 -m pip install PyYAML==6.0.2 + make lint-beta64-contract + lint-artifacthub: runs-on: ubuntu-latest container: diff --git a/AGENTS.md b/AGENTS.md index 1abb1ce..75935e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,8 @@ helm template test charts/authup -f charts/authup/ci/mysql-values.yaml helm template test charts/authup -f charts/authup/ci/split-values.yaml ``` -- **helm** >= 3.14 and **docker** (for the pinned generator images) required. +- **helm** >= 3.14, **python3** with PyYAML, and **docker** (for the pinned + generator images) required. - `charts/authup/README.md` and `charts/authup/values.schema.json` are GENERATED. Never edit them directly; edit `values.yaml` comments / `README.md.gotmpl` and run `make docs schema`. CI fails on drift diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c20791..a0d9b4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ 3. `values.schema.json` is GENERATED by helm-schema from `# @schema` annotations in `values.yaml`. Run `make schema`. 4. Before pushing: `make test` (lint + render every `ci/*-values.yaml` + - values-coverage check). CI fails on generated-file drift. + values coverage + beta.64 contract). CI fails on generated-file drift. 5. Breaking value changes: document the migration in `BREAKING.md` AND add a fail-loud tripwire for the old key in `templates/validations.yaml`. 6. Update the `artifacthub.io/changes` annotation in `Chart.yaml` for @@ -35,5 +35,6 @@ Then enable GitHub Pages for the `gh-pages` branch in the repo settings. ## Local tooling - helm >= 3.14 +- python3 with PyYAML (beta.64 rendered-manifest contract) - docker (helm-docs / helm-schema run via pinned images through `make`) - optional: `ct` (chart-testing) and a kind cluster for install tests diff --git a/DESIGN.md b/DESIGN.md index b7b06fb..68b4dbd 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -108,6 +108,12 @@ Generated Kubernetes Ingress resources deliberately target ingress-nginx and use its regex rewrite annotations. Gateway API HTTPRoutes use portable `URLRewrite` filters with `ReplacePrefixMatch`. +The three public console prefixes are fixed parts of the Authup beta.64 +contract, not chart values. Enabling a generated console Ingress or HTTPRoute +requires the corresponding server resource, which carries both the API and the +core-owned login/callback exceptions. A path-prefixed deployment-wide public +URL is rejected in split mode because it cannot preserve these root prefixes. + Each split console gets the shared `PUBLIC_URL` plus an `INTERNAL_URL` pointing at the core Service for server-side calls. Database, Redis, SMTP and bootstrap identity secrets never enter console pods. diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index 7897e74..01dfbff 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -28,7 +28,8 @@ land on the middle digit. Every entry lists the value migrations required. `/console/admin` and `/console/account`. The generated Ingress rules require ingress-nginx because they use regex prefix stripping. Gateway API users get portable `URLRewrite` filters. Exact admin/account login and callback paths - continue to route to the API. + continue to route to the API. Split mode does not support a path-prefixed + `server.publicUrl`. - `worker.enabled=true` creates the beta.64 background worker and sets `WORKER_ENABLED=false` on the API. The worker has no Service or HTTP probes. - When `server.networkPolicy.enabled=true`, the chart also creates hook-scoped diff --git a/charts/authup/README.md b/charts/authup/README.md index 2e8195f..59bc5be 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -90,24 +90,20 @@ server: authConsole: ingress: enabled: true - hostname: auth.example.com - tls: true adminConsole: enabled: true ingress: enabled: true - hostname: auth.example.com - tls: true accountConsole: enabled: true ingress: enabled: true - hostname: auth.example.com - tls: true ``` The split auth console is required because it owns the login flow. Disable the -admin or account console independently when those surfaces are not needed. +admin or account console independently when those surfaces are not needed. The +public URL must remain at the origin root; split mode reserves the `/console/*` +prefixes shown above. Notable operational facts (enforced or warned about by the chart): @@ -223,15 +219,13 @@ Kubernetes: `>=1.25.0-0` | accountConsole.hostAliases | list | `[]` | Pod host aliases | | accountConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | | accountConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | -| accountConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split account console | +| accountConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split account console (requires server.ingress.enabled) | | accountConsole.ingress.extraHosts | list | `[]` | Extra hosts | | accountConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | accountConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | accountConsole.ingress.extraTls | list | `[]` | Extra TLS entries | -| accountConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | +| accountConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) | | accountConsole.ingress.ingressClassName | string | `""` | Ingress class name | -| accountConsole.ingress.path | string | `"/console/account"` | Public console path (the generated ingress strips it) | -| accountConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | | accountConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | | accountConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | | accountConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | @@ -267,10 +261,8 @@ Kubernetes: `>=1.25.0-0` | accountConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Account console container resources | | accountConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | | accountConsole.route.annotations | object | `{}` | HTTPRoute annotations | -| accountConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the account console (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) | -| accountConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | +| accountConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the account console (requires server.route.enabled; 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) | | accountConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | -| accountConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | | accountConsole.route.parentRefs | list | `[]` | Gateway parentRefs | | accountConsole.schedulerName | string | `""` | Scheduler name | | accountConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) | @@ -319,15 +311,13 @@ Kubernetes: `>=1.25.0-0` | adminConsole.hostAliases | list | `[]` | Pod host aliases | | adminConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | | adminConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | -| adminConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split admin console | +| adminConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split admin console (requires server.ingress.enabled) | | adminConsole.ingress.extraHosts | list | `[]` | Extra hosts | | adminConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | adminConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | adminConsole.ingress.extraTls | list | `[]` | Extra TLS entries | -| adminConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | +| adminConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) | | adminConsole.ingress.ingressClassName | string | `""` | Ingress class name | -| adminConsole.ingress.path | string | `"/console/admin"` | Public console path (the generated ingress strips it) | -| adminConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | | adminConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | | adminConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | | adminConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | @@ -363,10 +353,8 @@ Kubernetes: `>=1.25.0-0` | adminConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Admin console 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 admin console (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.enabled | bool | `false` | Create a Gateway API HTTPRoute for the admin console (requires server.route.enabled; 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.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | -| adminConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | | adminConsole.route.parentRefs | list | `[]` | Gateway parentRefs | | adminConsole.schedulerName | string | `""` | Scheduler name | | adminConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) | @@ -426,15 +414,13 @@ Kubernetes: `>=1.25.0-0` | authConsole.hostAliases | list | `[]` | Pod host aliases | | authConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | | authConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | -| authConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split auth console | +| authConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split auth console (requires server.ingress.enabled) | | authConsole.ingress.extraHosts | list | `[]` | Extra hosts | | authConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | authConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | authConsole.ingress.extraTls | list | `[]` | Extra TLS entries | -| authConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL | +| authConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) | | authConsole.ingress.ingressClassName | string | `""` | Ingress class name | -| authConsole.ingress.path | string | `"/console/auth"` | Public console path (the generated ingress strips it) | -| authConsole.ingress.pathType | string | `"Prefix"` | Ingress path type | | authConsole.ingress.tls | bool | `false` | Enable TLS for the hostname | | authConsole.initContainers | list | `[]` | Init containers (tpl-rendered) | | authConsole.lifecycleHooks | object | `{}` | Container lifecycle hooks | @@ -470,10 +456,8 @@ Kubernetes: `>=1.25.0-0` | authConsole.resources | object | `{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | Auth console container resources | | authConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit | | authConsole.route.annotations | object | `{}` | HTTPRoute annotations | -| authConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the auth console (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) | -| authConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix | +| authConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the auth console (requires server.route.enabled; 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) | | authConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl) | -| authConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" | | authConsole.route.parentRefs | list | `[]` | Gateway parentRefs | | authConsole.schedulerName | string | `""` | Scheduler name | | authConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) | @@ -681,7 +665,7 @@ Kubernetes: `>=1.25.0-0` | server.service.sessionAffinityConfig | object | `{}` | Session affinity config | | server.service.type | string | `"ClusterIP"` | Service type | | server.sidecars | list | `[]` | Sidecar containers (tpl-rendered) | -| server.splitConsoles | bool | `false` | Run API-only server pods and deploy the console workloads separately | +| server.splitConsoles | bool | `false` | Run API-only server pods and deploy consoles separately (requires the public URL at the origin root because console prefixes are fixed) | | server.startupProbe.enabled | bool | `true` | Enable the startup probe (first boot runs database creation, migrations and provisioning) | | server.startupProbe.failureThreshold | int | `60` | | | server.startupProbe.initialDelaySeconds | int | `5` | | @@ -739,7 +723,7 @@ Kubernetes: `>=1.25.0-0` | worker.command | list | `[]` | Override the container command | | worker.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context | | worker.disableRestartOnChanges | bool | `false` | Disable checksum annotations that roll pods on configuration changes | -| worker.enabled | bool | `false` | Deploy a dedicated background worker | +| worker.enabled | bool | `false` | Deploy a dedicated background worker (requires server.enabled) | | worker.extraEnvVars | list | `[]` | Extra environment variables for the worker container | | worker.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | | worker.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | diff --git a/charts/authup/README.md.gotmpl b/charts/authup/README.md.gotmpl index 444f1ac..9a13ae2 100644 --- a/charts/authup/README.md.gotmpl +++ b/charts/authup/README.md.gotmpl @@ -103,24 +103,20 @@ server: authConsole: ingress: enabled: true - hostname: auth.example.com - tls: true adminConsole: enabled: true ingress: enabled: true - hostname: auth.example.com - tls: true accountConsole: enabled: true ingress: enabled: true - hostname: auth.example.com - tls: true ``` The split auth console is required because it owns the login flow. Disable the -admin or account console independently when those surfaces are not needed. +admin or account console independently when those surfaces are not needed. The +public URL must remain at the origin root; split mode reserves the `/console/*` +prefixes shown above. Notable operational facts (enforced or warned about by the chart): diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml index 9ebe83e..e277563 100644 --- a/charts/authup/ci/split-values.yaml +++ b/charts/authup/ci/split-values.yaml @@ -36,7 +36,6 @@ authConsole: allowExternalEgress: false ingress: enabled: true - hostname: auth.example.com resources: requests: cpu: 25m @@ -48,7 +47,6 @@ adminConsole: allowExternalEgress: false ingress: enabled: true - hostname: auth.example.com resources: requests: cpu: 25m @@ -60,7 +58,6 @@ accountConsole: allowExternalEgress: false ingress: enabled: true - hostname: auth.example.com resources: requests: cpu: 25m diff --git a/charts/authup/templates/NOTES.txt b/charts/authup/templates/NOTES.txt index 81adbaf..955522a 100644 --- a/charts/authup/templates/NOTES.txt +++ b/charts/authup/templates/NOTES.txt @@ -25,12 +25,12 @@ Runtime topology: Public URL / OIDC issuer: {{ $serverUrl }} OIDC discovery: {{ $serverUrl }}/.well-known/openid-configuration {{- if .Values.server.splitConsoles }} -Auth console: {{ $serverUrl }}{{ .Values.authConsole.ingress.path }} +Auth console: {{ $serverUrl }}/console/auth {{- if .Values.adminConsole.enabled }} -Admin console: {{ $serverUrl }}{{ .Values.adminConsole.ingress.path }} +Admin console: {{ $serverUrl }}/console/admin {{- end }} {{- if .Values.accountConsole.enabled }} -Account console: {{ $serverUrl }}{{ .Values.accountConsole.ingress.path }} +Account console: {{ $serverUrl }}/console/account {{- end }} {{- end }} {{- else }} diff --git a/charts/authup/templates/account-console/httproute.yaml b/charts/authup/templates/account-console/httproute.yaml index 34dc36e..fc855dc 100644 --- a/charts/authup/templates/account-console/httproute.yaml +++ b/charts/authup/templates/account-console/httproute.yaml @@ -32,23 +32,15 @@ spec: {{- end }} rules: - matches: - {{- if $route.matches }} - {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} - {{- else }} - path: type: PathPrefix value: /console/account - {{- end }} filters: - {{- if $route.filters }} - {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} - {{- else }} - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / - {{- end }} backendRefs: - name: {{ include "authup.accountConsole.fullname" . }} port: {{ .Values.accountConsole.service.ports.http }} diff --git a/charts/authup/templates/account-console/ingress.yaml b/charts/authup/templates/account-console/ingress.yaml index aab760e..810dd9d 100644 --- a/charts/authup/templates/account-console/ingress.yaml +++ b/charts/authup/templates/account-console/ingress.yaml @@ -23,7 +23,7 @@ spec: - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} http: paths: - - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + - path: /console/account(/|$)(.*) pathType: ImplementationSpecific backend: service: diff --git a/charts/authup/templates/admin-console/httproute.yaml b/charts/authup/templates/admin-console/httproute.yaml index 7a8f748..0fdaccd 100644 --- a/charts/authup/templates/admin-console/httproute.yaml +++ b/charts/authup/templates/admin-console/httproute.yaml @@ -32,23 +32,15 @@ spec: {{- end }} rules: - matches: - {{- if $route.matches }} - {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} - {{- else }} - path: type: PathPrefix value: /console/admin - {{- end }} filters: - {{- if $route.filters }} - {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} - {{- else }} - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / - {{- end }} backendRefs: - name: {{ include "authup.adminConsole.fullname" . }} port: {{ .Values.adminConsole.service.ports.http }} diff --git a/charts/authup/templates/admin-console/ingress.yaml b/charts/authup/templates/admin-console/ingress.yaml index 918784a..e31e77d 100644 --- a/charts/authup/templates/admin-console/ingress.yaml +++ b/charts/authup/templates/admin-console/ingress.yaml @@ -23,7 +23,7 @@ spec: - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} http: paths: - - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + - path: /console/admin(/|$)(.*) pathType: ImplementationSpecific backend: service: diff --git a/charts/authup/templates/auth-console/httproute.yaml b/charts/authup/templates/auth-console/httproute.yaml index 423c352..48eb3e9 100644 --- a/charts/authup/templates/auth-console/httproute.yaml +++ b/charts/authup/templates/auth-console/httproute.yaml @@ -32,23 +32,15 @@ spec: {{- end }} rules: - matches: - {{- if $route.matches }} - {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $) | nindent 8 }} - {{- else }} - path: type: PathPrefix value: /console/auth - {{- end }} filters: - {{- if $route.filters }} - {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $) | nindent 8 }} - {{- else }} - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / - {{- end }} backendRefs: - name: {{ include "authup.authConsole.fullname" . }} port: {{ .Values.authConsole.service.ports.http }} diff --git a/charts/authup/templates/auth-console/ingress.yaml b/charts/authup/templates/auth-console/ingress.yaml index 84e4460..0b00bcf 100644 --- a/charts/authup/templates/auth-console/ingress.yaml +++ b/charts/authup/templates/auth-console/ingress.yaml @@ -23,7 +23,7 @@ spec: - host: {{ include "authup.tplvalues.render" (dict "value" $hostname "context" $) | quote }} http: paths: - - path: {{ printf "%s(/|$)(.*)" ($ingress.path | trimSuffix "/") }} + - path: /console/auth(/|$)(.*) pathType: ImplementationSpecific backend: service: diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index 7c1c67b..807a722 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -55,6 +55,22 @@ database is configured while the mounted file is the operator's. {{- if and .Values.server.splitConsoles (not .Values.authConsole.enabled) }} {{- fail "authup: server.splitConsoles requires authConsole.enabled=true because the auth console owns the login flow." }} {{- end }} +{{- if .Values.server.splitConsoles }} +{{- $publicUrl := include "authup.server.publicUrl" . }} +{{- $path := (urlParse $publicUrl).path }} +{{- if and $path (ne $path "/") }} +{{- fail "authup: server.splitConsoles requires server.publicUrl at the origin root because the console routes use fixed /console/* paths." }} +{{- end }} +{{- end }} +{{- if and .Values.worker.enabled (not .Values.server.enabled) }} +{{- fail "authup: worker.enabled requires server.enabled=true because the worker shares the server configuration and credentials." }} +{{- end }} +{{- if and .Values.server.splitConsoles (or (and .Values.authConsole.enabled .Values.authConsole.ingress.enabled) (and .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled) (and .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled)) (not .Values.server.ingress.enabled) }} +{{- fail "authup: split console Ingress requires server.ingress.enabled=true so core owns the API and login callback paths." }} +{{- end }} +{{- if and .Values.server.splitConsoles (or (and .Values.authConsole.enabled (include "authup.flag" (dict "value" .Values.authConsole.route.enabled "context" $ "key" "authConsole.route.enabled"))) (and .Values.adminConsole.enabled (include "authup.flag" (dict "value" .Values.adminConsole.route.enabled "context" $ "key" "adminConsole.route.enabled"))) (and .Values.accountConsole.enabled (include "authup.flag" (dict "value" .Values.accountConsole.route.enabled "context" $ "key" "accountConsole.route.enabled")))) (not (include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled"))) }} +{{- fail "authup: split console HTTPRoute requires server.route.enabled=true so core owns the API and login callback paths." }} +{{- end }} {{/* An external database password must be deliberate — never silently generated for a database the chart does not own. */}} {{- if and (not .Values.postgresql.enabled) (not .Values.mysql.enabled) .Values.externalDatabase.host (not .Values.externalDatabase.password) (not .Values.externalDatabase.existingSecret) }} @@ -64,14 +80,14 @@ database is configured while the mounted file is the operator's. {{- if and .Values.server.ingress.enabled (not .Values.server.ingress.hostname) (not .Values.server.ingress.extraHosts) (not .Values.server.ingress.extraRules) }} {{- fail "authup: server.ingress.enabled requires server.ingress.hostname (or extraHosts / extraRules)." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled (not .Values.authConsole.ingress.hostname) (not .Values.authConsole.ingress.extraHosts) (not .Values.authConsole.ingress.extraRules) }} -{{- fail "authup: authConsole.ingress.enabled requires authConsole.ingress.hostname (or extraHosts / extraRules)." }} +{{- if and .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled (not .Values.authConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.authConsole.ingress.extraHosts) (not .Values.authConsole.ingress.extraRules) }} +{{- fail "authup: authConsole.ingress.enabled requires authConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.adminConsole.ingress.extraHosts) (not .Values.adminConsole.ingress.extraRules) }} -{{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname (or extraHosts / extraRules)." }} +{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.adminConsole.ingress.extraHosts) (not .Values.adminConsole.ingress.extraRules) }} +{{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled (not .Values.accountConsole.ingress.hostname) (not .Values.accountConsole.ingress.extraHosts) (not .Values.accountConsole.ingress.extraRules) }} -{{- fail "authup: accountConsole.ingress.enabled requires accountConsole.ingress.hostname (or extraHosts / extraRules)." }} +{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled (not .Values.accountConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.accountConsole.ingress.extraHosts) (not .Values.accountConsole.ingress.extraRules) }} +{{- fail "authup: accountConsole.ingress.enabled requires accountConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} {{- end }} {{/* diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 764ec7a..5859b54 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -297,7 +297,7 @@ }, "enabled": { "default": false, - "description": "Enable ingress-nginx routing for the split account console", + "description": "Enable ingress-nginx routing for the split account console (requires server.ingress.enabled)", "required": [], "title": "enabled", "type": "boolean" @@ -340,7 +340,7 @@ }, "hostname": { "default": "", - "description": "Ingress hostname (tpl-rendered); also drives the derived UI public URL", + "description": "Ingress hostname (tpl-rendered; defaults to server.ingress.hostname)", "required": [], "title": "hostname", "type": "string" @@ -352,20 +352,6 @@ "title": "ingressClassName", "type": "string" }, - "path": { - "default": "/console/account", - "description": "Public console path (the generated ingress strips it)", - "required": [], - "title": "path", - "type": "string" - }, - "pathType": { - "default": "Prefix", - "description": "Ingress path type", - "required": [], - "title": "pathType", - "type": "string" - }, "tls": { "default": false, "description": "Enable TLS for the hostname", @@ -377,8 +363,6 @@ "required": [ "enabled", "hostname", - "path", - "pathType", "ingressClassName", "tls", "certManager", @@ -732,7 +716,7 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for the account console (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)", + "description": "Create a Gateway API HTTPRoute for the account console (requires server.route.enabled;\ntpl-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": [ @@ -740,15 +724,6 @@ "string" ] }, - "filters": { - "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", - "items": { - "required": [] - }, - "required": [], - "title": "filters", - "type": "array" - }, "hostnames": { "description": "Route hostnames ([] = derived from server.publicUrl)", "items": { @@ -758,15 +733,6 @@ "title": "hostnames", "type": "array" }, - "matches": { - "description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"", - "items": { - "required": [] - }, - "required": [], - "title": "matches", - "type": "array" - }, "parentRefs": { "description": "Gateway parentRefs", "items": { @@ -780,9 +746,7 @@ "required": [ "enabled", "hostnames", - "parentRefs", - "matches", - "filters" + "parentRefs" ], "title": "route", "type": "object" @@ -1344,7 +1308,7 @@ }, "enabled": { "default": false, - "description": "Enable ingress-nginx routing for the split admin console", + "description": "Enable ingress-nginx routing for the split admin console (requires server.ingress.enabled)", "required": [], "title": "enabled", "type": "boolean" @@ -1387,7 +1351,7 @@ }, "hostname": { "default": "", - "description": "Ingress hostname (tpl-rendered); also drives the derived UI public URL", + "description": "Ingress hostname (tpl-rendered; defaults to server.ingress.hostname)", "required": [], "title": "hostname", "type": "string" @@ -1399,20 +1363,6 @@ "title": "ingressClassName", "type": "string" }, - "path": { - "default": "/console/admin", - "description": "Public console path (the generated ingress strips it)", - "required": [], - "title": "path", - "type": "string" - }, - "pathType": { - "default": "Prefix", - "description": "Ingress path type", - "required": [], - "title": "pathType", - "type": "string" - }, "tls": { "default": false, "description": "Enable TLS for the hostname", @@ -1424,8 +1374,6 @@ "required": [ "enabled", "hostname", - "path", - "pathType", "ingressClassName", "tls", "certManager", @@ -1779,7 +1727,7 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for the admin console (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)", + "description": "Create a Gateway API HTTPRoute for the admin console (requires server.route.enabled;\ntpl-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": [ @@ -1787,15 +1735,6 @@ "string" ] }, - "filters": { - "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", - "items": { - "required": [] - }, - "required": [], - "title": "filters", - "type": "array" - }, "hostnames": { "description": "Route hostnames ([] = derived from server.publicUrl)", "items": { @@ -1805,15 +1744,6 @@ "title": "hostnames", "type": "array" }, - "matches": { - "description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"", - "items": { - "required": [] - }, - "required": [], - "title": "matches", - "type": "array" - }, "parentRefs": { "description": "Gateway parentRefs", "items": { @@ -1827,9 +1757,7 @@ "required": [ "enabled", "hostnames", - "parentRefs", - "matches", - "filters" + "parentRefs" ], "title": "route", "type": "object" @@ -2500,7 +2428,7 @@ }, "enabled": { "default": false, - "description": "Enable ingress-nginx routing for the split auth console", + "description": "Enable ingress-nginx routing for the split auth console (requires server.ingress.enabled)", "required": [], "title": "enabled", "type": "boolean" @@ -2543,7 +2471,7 @@ }, "hostname": { "default": "", - "description": "Ingress hostname (tpl-rendered); also drives the derived UI public URL", + "description": "Ingress hostname (tpl-rendered; defaults to server.ingress.hostname)", "required": [], "title": "hostname", "type": "string" @@ -2555,20 +2483,6 @@ "title": "ingressClassName", "type": "string" }, - "path": { - "default": "/console/auth", - "description": "Public console path (the generated ingress strips it)", - "required": [], - "title": "path", - "type": "string" - }, - "pathType": { - "default": "Prefix", - "description": "Ingress path type", - "required": [], - "title": "pathType", - "type": "string" - }, "tls": { "default": false, "description": "Enable TLS for the hostname", @@ -2580,8 +2494,6 @@ "required": [ "enabled", "hostname", - "path", - "pathType", "ingressClassName", "tls", "certManager", @@ -2935,7 +2847,7 @@ }, "enabled": { "default": false, - "description": "Create a Gateway API HTTPRoute for the auth console (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)", + "description": "Create a Gateway API HTTPRoute for the auth console (requires server.route.enabled;\ntpl-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": [ @@ -2943,15 +2855,6 @@ "string" ] }, - "filters": { - "description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix", - "items": { - "required": [] - }, - "required": [], - "title": "filters", - "type": "array" - }, "hostnames": { "description": "Route hostnames ([] = derived from server.publicUrl)", "items": { @@ -2961,15 +2864,6 @@ "title": "hostnames", "type": "array" }, - "matches": { - "description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"", - "items": { - "required": [] - }, - "required": [], - "title": "matches", - "type": "array" - }, "parentRefs": { "description": "Gateway parentRefs", "items": { @@ -2983,9 +2877,7 @@ "required": [ "enabled", "hostnames", - "parentRefs", - "matches", - "filters" + "parentRefs" ], "title": "route", "type": "object" @@ -5394,7 +5286,7 @@ }, "splitConsoles": { "default": false, - "description": "Run API-only server pods and deploy the console workloads separately", + "description": "Run API-only server pods and deploy consoles separately (requires the\npublic URL at the origin root because console prefixes are fixed)", "required": [], "title": "splitConsoles", "type": "boolean" @@ -6207,7 +6099,7 @@ }, "enabled": { "default": false, - "description": "Deploy a dedicated background worker", + "description": "Deploy a dedicated background worker (requires server.enabled)", "required": [], "title": "enabled", "type": "boolean" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 95e697e..3597e62 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -403,7 +403,8 @@ smtp: server: # -- Deploy the server-core service enabled: true - # -- Run API-only server pods and deploy the console workloads separately + # -- Run API-only server pods and deploy consoles separately (requires the + # public URL at the origin root because console prefixes are fixed) splitConsoles: false # -- Number of server-core replicas (values > 1 REQUIRE a configured cache) replicaCount: 1 @@ -1030,14 +1031,10 @@ authConsole: # -- Extra service ports extraPorts: [] ingress: - # -- Enable ingress-nginx routing for the split auth console + # -- Enable ingress-nginx routing for the split auth console (requires server.ingress.enabled) enabled: false - # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL + # -- Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) hostname: "" - # -- Public console path (the generated ingress strips it) - path: /console/auth - # -- Ingress path type - pathType: Prefix # -- Ingress class name ingressClassName: "" # @schema @@ -1062,7 +1059,8 @@ authConsole: # type: [boolean, string] # required: true # @schema - # -- Create a Gateway API HTTPRoute for the auth console (tpl-rendered: a string rendering to + # -- Create a Gateway API HTTPRoute for the auth console (requires server.route.enabled; + # 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) enabled: false @@ -1075,10 +1073,6 @@ authConsole: # @schema # -- HTTPRoute annotations annotations: {} - # -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" - matches: [] - # -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix - filters: [] pdb: # -- Create a PodDisruptionBudget for the UI create: false @@ -1309,14 +1303,10 @@ adminConsole: # -- Extra service ports extraPorts: [] ingress: - # -- Enable ingress-nginx routing for the split admin console + # -- Enable ingress-nginx routing for the split admin console (requires server.ingress.enabled) enabled: false - # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL + # -- Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) hostname: "" - # -- Public console path (the generated ingress strips it) - path: /console/admin - # -- Ingress path type - pathType: Prefix # -- Ingress class name ingressClassName: "" # @schema @@ -1341,7 +1331,8 @@ adminConsole: # type: [boolean, string] # required: true # @schema - # -- Create a Gateway API HTTPRoute for the admin console (tpl-rendered: a string rendering to + # -- Create a Gateway API HTTPRoute for the admin console (requires server.route.enabled; + # 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) enabled: false @@ -1354,10 +1345,6 @@ adminConsole: # @schema # -- HTTPRoute annotations annotations: {} - # -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" - matches: [] - # -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix - filters: [] pdb: # -- Create a PodDisruptionBudget for the UI create: false @@ -1588,14 +1575,10 @@ accountConsole: # -- Extra service ports extraPorts: [] ingress: - # -- Enable ingress-nginx routing for the split account console + # -- Enable ingress-nginx routing for the split account console (requires server.ingress.enabled) enabled: false - # -- Ingress hostname (tpl-rendered); also drives the derived UI public URL + # -- Ingress hostname (tpl-rendered; defaults to server.ingress.hostname) hostname: "" - # -- Public console path (the generated ingress strips it) - path: /console/account - # -- Ingress path type - pathType: Prefix # -- Ingress class name ingressClassName: "" # @schema @@ -1620,7 +1603,8 @@ accountConsole: # type: [boolean, string] # required: true # @schema - # -- Create a Gateway API HTTPRoute for the account console (tpl-rendered: a string rendering to + # -- Create a Gateway API HTTPRoute for the account console (requires server.route.enabled; + # 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) enabled: false @@ -1633,10 +1617,6 @@ accountConsole: # @schema # -- HTTPRoute annotations annotations: {} - # -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" - matches: [] - # -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix - filters: [] pdb: # -- Create a PodDisruptionBudget for the UI create: false @@ -1681,7 +1661,7 @@ accountConsole: ## @section Worker worker: - # -- Deploy a dedicated background worker + # -- Deploy a dedicated background worker (requires server.enabled) enabled: false # -- Number of worker replicas (one is normally sufficient) replicaCount: 1 diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 4bad669..9f9589a 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -368,10 +368,58 @@ def check_policy(): def check_validations(): + render( + { + "server": { + "splitConsoles": True, + "ingress": {"enabled": True, "hostname": "auth.example.com"}, + }, + "authConsole": {"ingress": {"enabled": True}}, + "adminConsole": {"ingress": {"enabled": True}}, + "accountConsole": {"ingress": {"enabled": True}}, + } + ) render_fails( {"server": {"features": {"accountConsole": False}}}, "server.features.accountConsole moved to accountConsole.enabled", ) + render_fails( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + }, + "adminConsole": { + "ingress": {"enabled": True, "hostname": "auth.example.com"} + }, + }, + "split console Ingress requires server.ingress.enabled=true", + ) + render_fails( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + }, + "adminConsole": {"route": {"enabled": True}}, + }, + "split console HTTPRoute requires server.route.enabled=true", + ) + for server in ( + {"splitConsoles": True, "publicUrl": "https://auth.example.com/prefix"}, + { + "splitConsoles": True, + "ingress": { + "enabled": True, + "hostname": "auth.example.com", + "path": "/prefix", + }, + }, + ): + render_fails( + {"server": server}, + "server.splitConsoles requires server.publicUrl at the origin root", + ) render_fails( {"server": {"enabled": False, "splitConsoles": True}}, "server.splitConsoles requires server.enabled=true", @@ -380,6 +428,10 @@ def check_validations(): {"server": {"splitConsoles": True}, "authConsole": {"enabled": False}}, "server.splitConsoles requires authConsole.enabled=true", ) + render_fails( + {"server": {"enabled": False}, "worker": {"enabled": True}}, + "worker.enabled requires server.enabled=true", + ) for component in ("authConsole", "adminConsole", "accountConsole"): render_fails( {component: {"enabled": False, "route": {"enabled": "invalid"}}}, From e735945ac15bfe6e988b3dd1e7746f498520a9a1 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 10:30:47 +0200 Subject: [PATCH 09/14] fix(authup): harden split topology boundaries Keep restricted server ingress usable for same-namespace callers and the CI ingress controller. Reject split routes that diverge from the shared origin, and isolate migration hooks from server-only extension inputs. --- charts/authup/ci/split-values.yaml | 2 + .../templates/server/migration-job.yaml | 22 +---- .../templates/server/networkpolicy.yaml | 1 + charts/authup/templates/validations.yaml | 27 ++++++ scripts/check-beta64-contract.py | 89 +++++++++++++++++++ 5 files changed, 121 insertions(+), 20 deletions(-) diff --git a/charts/authup/ci/split-values.yaml b/charts/authup/ci/split-values.yaml index e277563..ec35f6a 100644 --- a/charts/authup/ci/split-values.yaml +++ b/charts/authup/ci/split-values.yaml @@ -17,6 +17,8 @@ server: enabled: true allowExternal: false allowExternalEgress: false + ingressNSMatchLabels: + kubernetes.io/metadata.name: ingress-nginx resources: requests: cpu: 50m diff --git a/charts/authup/templates/server/migration-job.yaml b/charts/authup/templates/server/migration-job.yaml index 41bbd8c..3e800eb 100644 --- a/charts/authup/templates/server/migration-job.yaml +++ b/charts/authup/templates/server/migration-job.yaml @@ -60,36 +60,18 @@ spec: args: - migration - run - {{- if or .Values.server.extraEnvVarsCM .Values.server.extraEnvVarsSecret }} - envFrom: - {{- if .Values.server.extraEnvVarsCM }} - - configMapRef: - name: {{ include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVarsCM "context" $) }} - {{- end }} - {{- if .Values.server.extraEnvVarsSecret }} - - secretRef: - name: {{ include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVarsSecret "context" $) }} - {{- end }} - {{- end }} env: {{- range $key, $value := (include "authup.server.configEnv" . | fromYaml) }} + {{- if ne $key "PROVISIONING_DIRECTORY_PATH" }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} - {{- include "authup.server.secretEnv" (dict "context" $ "role" "migration") | nindent 12 }} - {{- if .Values.server.extraEnvVars }} - {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraEnvVars "context" $) | nindent 12 }} {{- end }} + {{- include "authup.server.secretEnv" (dict "context" $ "role" "migration") | nindent 12 }} {{- $resources := .Values.server.migration.resources | default .Values.server.resources }} {{- if $resources }} resources: {{- toYaml $resources | nindent 12 }} {{- end }} volumeMounts: {{- include "authup.server.volumeMounts" (dict "context" $ "role" "migration") | nindent 12 }} - {{- if .Values.server.extraVolumeMounts }} - {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumeMounts "context" $) | nindent 12 }} - {{- end }} volumes: {{- include "authup.server.volumes" (dict "context" $ "role" "migration") | nindent 8 }} - {{- if .Values.server.extraVolumes }} - {{- include "authup.tplvalues.render" (dict "value" .Values.server.extraVolumes "context" $) | nindent 8 }} - {{- end }} {{- end }} diff --git a/charts/authup/templates/server/networkpolicy.yaml b/charts/authup/templates/server/networkpolicy.yaml index a823c10..52db1e7 100644 --- a/charts/authup/templates/server/networkpolicy.yaml +++ b/charts/authup/templates/server/networkpolicy.yaml @@ -18,6 +18,7 @@ spec: protocol: TCP {{- if not .Values.server.networkPolicy.allowExternal }} from: + - podSelector: {} {{- if .Values.server.splitConsoles }} {{- if .Values.authConsole.enabled }} - podSelector: diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index 807a722..e1eb181 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -61,6 +61,33 @@ database is configured while the mounted file is the operator's. {{- if and $path (ne $path "/") }} {{- fail "authup: server.splitConsoles requires server.publicUrl at the origin root because the console routes use fixed /console/* paths." }} {{- end }} +{{- $publicHostname := regexReplaceAll ":[0-9]+$" (urlParse $publicUrl).host "" }} +{{- $serverRouteEnabled := include "authup.flag" (dict "value" .Values.server.route.enabled "context" $ "key" "server.route.enabled") }} +{{- if $serverRouteEnabled }} +{{- range .Values.server.route.hostnames }} +{{- $hostname := include "authup.tplvalues.render" (dict "value" . "context" $) }} +{{- if ne $hostname $publicHostname }} +{{- fail "authup: server.route.hostnames must match server.publicUrl in split-console mode." }} +{{- end }} +{{- end }} +{{- end }} +{{- range $name, $console := dict "authConsole" .Values.authConsole "adminConsole" .Values.adminConsole "accountConsole" .Values.accountConsole }} +{{- if and $.Values.server.ingress.enabled $console.enabled $console.ingress.enabled $console.ingress.hostname }} +{{- $serverIngressHostname := include "authup.tplvalues.render" (dict "value" $.Values.server.ingress.hostname "context" $) }} +{{- $hostname := include "authup.tplvalues.render" (dict "value" $console.ingress.hostname "context" $) }} +{{- if ne $hostname $serverIngressHostname }} +{{- fail (printf "authup: %s.ingress.hostname must match server.ingress.hostname in split-console mode." $name) }} +{{- end }} +{{- end }} +{{- if and $serverRouteEnabled $console.enabled (include "authup.flag" (dict "value" $console.route.enabled "context" $ "key" (printf "%s.route.enabled" $name))) }} +{{- range $console.route.hostnames }} +{{- $hostname := include "authup.tplvalues.render" (dict "value" . "context" $) }} +{{- if ne $hostname $publicHostname }} +{{- fail (printf "authup: %s.route.hostnames must match server.publicUrl in split-console mode." $name) }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} {{- end }} {{- if and .Values.worker.enabled (not .Values.server.enabled) }} {{- fail "authup: worker.enabled requires server.enabled=true because the worker shares the server configuration and credentials." }} diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 9f9589a..6642fe8 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -183,6 +183,19 @@ def check_base(): migration_values = { "server": { "configuration": "core:\n trustProxy: '1'\n", + "provisioning": { + "enabled": True, + "files": {"realms.yaml": "[]\n"}, + }, + "extraEnvVarsCM": "server-only-env", + "extraEnvVarsSecret": "server-only-secret", + "extraEnvVars": [{"name": "SERVER_ONLY", "value": "true"}], + "extraVolumeMounts": [ + {"name": "server-only", "mountPath": "/server-only"} + ], + "extraVolumes": [ + {"name": "server-only", "configMap": {"name": "server-only"}} + ], "migration": {"enabled": True}, } } @@ -195,6 +208,14 @@ def check_base(): "authup.yml", ) assert migration_mounts["logs"][0] == "/var/log/authup" + assert set(migration_mounts) == {"configuration", "logs", "tmp"} + assert "envFrom" not in container(migration) + migration_env = effective_env(migration, migration_documents) + assert "PROVISIONING_DIRECTORY_PATH" not in migration_env + assert "SERVER_ONLY" not in migration_env + assert { + volume["name"] for volume in migration["spec"]["template"]["spec"]["volumes"] + } == {"configuration", "logs", "tmp"} def check_split(): @@ -354,6 +375,14 @@ def check_policy(): "admin-console", "account-console", } <= peer_components(server, "ingress") + ingress_peers = server["spec"]["ingress"][0]["from"] + assert any(peer.get("podSelector") == {} for peer in ingress_peers) + assert any( + peer.get("namespaceSelector", {}).get("matchLabels", {}).get( + "kubernetes.io/metadata.name" + ) == "ingress-nginx" + for peer in ingress_peers + ) argocd_documents = render( chart / "ci" / "split-values.yaml", @@ -405,6 +434,66 @@ def check_validations(): }, "split console HTTPRoute requires server.route.enabled=true", ) + for component in ("authConsole", "adminConsole", "accountConsole"): + render_fails( + { + "server": { + "splitConsoles": True, + "ingress": { + "enabled": True, + "hostname": "auth.example.com", + }, + }, + component: { + "ingress": { + "enabled": True, + "hostname": "other.example.com", + } + }, + }, + f"{component}.ingress.hostname must match server.ingress.hostname", + ) + render_fails( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + "route": {"enabled": True}, + }, + component: { + "route": { + "enabled": True, + "hostnames": ["other.example.com"], + } + }, + }, + f"{component}.route.hostnames must match server.publicUrl", + ) + render_fails( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + "route": { + "enabled": True, + "hostnames": ["other.example.com"], + }, + }, + "authConsole": {"route": {"enabled": True}}, + }, + "server.route.hostnames must match server.publicUrl", + ) + render( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + "ingress": {"hostname": '{{ fail "inactive ingress" }}'}, + "route": {"enabled": True}, + }, + "authConsole": {"route": {"enabled": True}}, + } + ) for server in ( {"splitConsoles": True, "publicUrl": "https://auth.example.com/prefix"}, { From 6fd006ab0480832dd288c1360fca3d172d3a28a3 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 10:44:25 +0200 Subject: [PATCH 10/14] fix(ci): repair beta64 contract checks Install PyYAML with uv inside chart-testing's active virtualenv and require the same-namespace NetworkPolicy peer to be distinct. --- .github/workflows/lint-test.yaml | 2 +- scripts/check-beta64-contract.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index f9fbf95..2616f77 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -49,7 +49,7 @@ jobs: - name: Check Authup beta.64 contract run: | - python3 -m pip install PyYAML==6.0.2 + uv pip install PyYAML==6.0.2 make lint-beta64-contract lint-artifacthub: diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 6642fe8..a6108b4 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -376,7 +376,10 @@ def check_policy(): "account-console", } <= peer_components(server, "ingress") ingress_peers = server["spec"]["ingress"][0]["from"] - assert any(peer.get("podSelector") == {} for peer in ingress_peers) + assert any( + peer.get("podSelector") == {} and "namespaceSelector" not in peer + for peer in ingress_peers + ) assert any( peer.get("namespaceSelector", {}).get("matchLabels", {}).get( "kubernetes.io/metadata.name" From 400139504a96fe7815a8f0ecc7ecdaf58daba1c9 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 11:06:28 +0200 Subject: [PATCH 11/14] fix(authup): migrate ephemeral databases after upgrades Keep in-process migration enabled when an upgrade replaces a non-persistent built-in database after the pre-upgrade hook. Preserve serialized hook ownership for persistent and external databases. Cover both migration paths in the beta.64 render contract and update generated documentation. --- .agents/architecture.md | 3 ++- .agents/testing.md | 3 ++- DESIGN.md | 4 +++- charts/authup/README.md | 2 +- charts/authup/templates/server/deployment.yaml | 2 +- charts/authup/values.schema.json | 2 +- charts/authup/values.yaml | 2 +- scripts/check-beta64-contract.py | 5 ++++- 8 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.agents/architecture.md b/.agents/architecture.md index 6856286..a3e1302 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -91,7 +91,8 @@ operational invariants that template changes must preserve. 20. **The migration Job is pre-upgrade only.** Fresh installs need regular backing resources before the server can initialize the database. On upgrades, the Job runs before the rollout. The server sets - `MIGRATION_ENABLED=false` only during upgrades when this Job owns migration. + `MIGRATION_ENABLED=false` only when the migrated database persists through + the rollout; non-persistent built-in databases initialize again at boot. 21. **Hook inputs must exist before regular resources.** The migration Job inlines non-secret config, narrows secrets to database password and optional encryption key, skips provisioning, and mounts a hook-scoped copy of diff --git a/.agents/testing.md b/.agents/testing.md index 8c2719c..e249f83 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -64,7 +64,8 @@ The pre-upgrade migration Job must stay narrower than the server Deployment: - the migration NetworkPolicy selects component `migration`, uses the same hook family, and runs at weight or wave -5 before the Job at 0 - fresh-install server env has no `MIGRATION_ENABLED`; upgrade server env has - `MIGRATION_ENABLED=false` when the Job is enabled + `MIGRATION_ENABLED=false` when the Job is enabled and the database persists, + but leaves startup migration enabled for non-persistent built-in databases Run both Helm and ArgoCD annotation paths: diff --git a/DESIGN.md b/DESIGN.md index 68b4dbd..87f68ab 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -167,7 +167,9 @@ startup and its generous startup probe. `server.migration.enabled=true` creates a pre-upgrade Job. On upgrade, core gets `MIGRATION_ENABLED=false` and the Job owns schema migration before pods roll. -This avoids concurrent DDL, especially with multiple API replicas and MySQL. +Non-persistent built-in databases are the exception: their rollout replaces the +database after the hook, so core initializes the replacement at boot. This +avoids concurrent DDL when the migrated database survives the rollout. Helm creates hooks before regular release resources. The Job therefore: diff --git a/charts/authup/README.md b/charts/authup/README.md index 59bc5be..7470e0d 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -612,7 +612,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 migrate at boot regardless. | +| 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. | | 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) | diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index 428bd98..dee869f 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -128,7 +128,7 @@ spec: - name: WORKER_ENABLED value: "false" {{- end }} - {{- if and .Values.server.migration.enabled .Release.IsUpgrade }} + {{- if and .Values.server.migration.enabled .Release.IsUpgrade (or (not (or .Values.postgresql.enabled .Values.mysql.enabled)) (and .Values.postgresql.enabled .Values.postgresql.persistence.enabled) (and .Values.mysql.enabled .Values.mysql.persistence.enabled)) }} - name: MIGRATION_ENABLED value: "false" {{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index 5859b54..e502c7d 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -4742,7 +4742,7 @@ }, "enabled": { "default": false, - "description": "Run `migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls migrate at boot regardless.", + "description": "Run `migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls and non-persistent built-in databases migrate at boot regardless.", "required": [], "title": "enabled", "type": "boolean" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 3597e62..66051d4 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -527,7 +527,7 @@ server: migration: # -- Run `migration run` as a pre-upgrade hook Job. Recommended # for multi-replica deployments (serializes DDL before pods roll). Fresh - # installs migrate at boot regardless. + # installs and non-persistent built-in databases migrate at boot regardless. enabled: false # -- Job backoff limit backoffLimit: 3 diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index a6108b4..1027422 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -216,6 +216,9 @@ def check_base(): assert { volume["name"] for volume in migration["spec"]["template"]["spec"]["volumes"] } == {"configuration", "logs", "tmp"} + migration_upgrade = render(migration_values, "--is-upgrade") + migration_server = one(migration_upgrade, "Deployment", "server") + assert effective_env(migration_server, migration_upgrade)["MIGRATION_ENABLED"] == "false" def check_split(): @@ -268,7 +271,7 @@ def check_split(): upgrade = render(chart / "ci" / "split-values.yaml", "--is-upgrade") upgrade_server = one(upgrade, "Deployment", "server") - assert effective_env(upgrade_server, upgrade)["MIGRATION_ENABLED"] == "false" + assert "MIGRATION_ENABLED" not in effective_env(upgrade_server, upgrade) assert "MIGRATION_ENABLED" not in effective_env(deployments["server"], documents) rendered = render_result(chart / "ci" / "split-values.yaml").stdout From 15a89b4eea718e171d45d5ae9c8a8018b3d21823 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 14:34:51 +0200 Subject: [PATCH 12/14] fix(authup): close review gaps in the beta.64 topology Correctness: - migration NetworkPolicy drops hook-succeeded: Helm 4 deletes earlier succeeded hooks when a later hook fails, cutting off a running Job pod - MIGRATION_ENABLED=false also applies under useHelmHooks=false, where PreSync precedes every sync but .Release.IsUpgrade is never true, and treats persistence.existingClaim as a persisting database - split consoles receive TRUSTED_ORIGINS; the console config collision guard referenced .key inside a range and died with a raw template error - worker Deployment gains the database and Redis secret checksums - console Ingress: chart-owned rewrite annotations are no longer duplicated by user annotations, certManager adds the tls-acme annotation it documented, and the never-rendered ingress.extraHosts key is removed - server NetworkPolicy returns to the allowlist model (no bare podSelector), renders the port rule only when it has a peer, and fails allowExternal=false without any ingress source Guards: split mode requires a public URL; server.config WRITABLE_DIRECTORY_PATH is a tripwire. Docs: theme wording, core.middlewarePrometheus hint, valkey fixture core.logger, BREAKING.md removed keys, worker fresh-install behaviour, ArgoCD first-sync limitation (#30). Contract script asserts the new boundaries. --- .agents/architecture.md | 3 +- .agents/testing.md | 6 +- DESIGN.md | 5 +- charts/authup/BREAKING.md | 14 ++-- charts/authup/README.md | 49 ++++++------ charts/authup/README.md.gotmpl | 2 +- charts/authup/ci/theme-values.yaml | 2 - charts/authup/ci/valkey-values.yaml | 3 +- charts/authup/templates/NOTES.txt | 2 +- charts/authup/templates/_console-env.tpl | 8 +- charts/authup/templates/_ingress.tpl | 2 +- .../templates/account-console/ingress.yaml | 7 +- .../templates/admin-console/ingress.yaml | 7 +- .../templates/auth-console/ingress.yaml | 7 +- .../templates/server/configmap-env.yaml | 2 +- .../authup/templates/server/deployment.yaml | 6 +- .../server/migration-networkpolicy.yaml | 4 +- .../templates/server/networkpolicy.yaml | 5 +- charts/authup/templates/validations.yaml | 22 ++++-- .../authup/templates/worker/deployment.yaml | 6 ++ charts/authup/values.schema.json | 74 ++++++------------- charts/authup/values.yaml | 66 +++++++++-------- scripts/check-beta64-contract.py | 57 +++++++++++++- 23 files changed, 211 insertions(+), 148 deletions(-) diff --git a/.agents/architecture.md b/.agents/architecture.md index a3e1302..c1f9596 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -90,7 +90,8 @@ operational invariants that template changes must preserve. cross-revision name audit; a renamed kept Secret rotates credentials. 20. **The migration Job is pre-upgrade only.** Fresh installs need regular backing resources before the server can initialize the database. On - upgrades, the Job runs before the rollout. The server sets + upgrades, and on every ArgoCD sync when `useHelmHooks=false`, the Job runs + before the rollout. The server sets `MIGRATION_ENABLED=false` only when the migrated database persists through the rollout; non-persistent built-in databases initialize again at boot. 21. **Hook inputs must exist before regular resources.** The migration Job diff --git a/.agents/testing.md b/.agents/testing.md index e249f83..e516832 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -37,7 +37,7 @@ and cache, plus restrictive NetworkPolicies. - no database, both built-in databases, or an unknown external database type - API replicas/HPA without Redis - MFA required while MFA is disabled -- inline and existing-secret carriers set together +- inline and existing-secret auth carriers set together - scheme-less public URLs or an Ingress without a hostname - a `server.config` key owned by a first-class value - both inline and existing configuration ConfigMaps @@ -65,7 +65,9 @@ The pre-upgrade migration Job must stay narrower than the server Deployment: family, and runs at weight or wave -5 before the Job at 0 - 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 + 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 Run both Helm and ArgoCD annotation paths: diff --git a/DESIGN.md b/DESIGN.md index 87f68ab..640934c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -165,8 +165,9 @@ Authup core can initialize and migrate at startup. A pre-install migration Job would run before chart-managed databases exist, so fresh installs rely on core startup and its generous startup probe. -`server.migration.enabled=true` creates a pre-upgrade Job. On upgrade, core gets -`MIGRATION_ENABLED=false` and the Job owns schema migration before pods roll. +`server.migration.enabled=true` creates a pre-upgrade Job. On upgrade, and on +every ArgoCD sync when `useHelmHooks=false`, core gets `MIGRATION_ENABLED=false` +and the Job owns schema migration before pods roll. Non-persistent built-in databases are the exception: their rollout replaces the database after the hook, so core initializes the replacement at boot. This avoids concurrent DDL when the migrated database survives the rollout. diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index 01dfbff..d1e4cf0 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -16,13 +16,17 @@ land on the middle digit. Every entry lists the value migrations required. remain independently optional. - `server.features.accountConsole` moves to `accountConsole.enabled`. Any non-empty old value now fails the render with the replacement key. -- `adminConsole.publicUrl`, `adminConsole.apiUrl` and - `server.trustedOriginsAppendAdminConsole` are removed. All roles share - `server.publicUrl`; split console server-side requests use the generated - in-cluster `INTERNAL_URL`. +- `adminConsole.publicUrl`, `adminConsole.apiUrl`, `adminConsole.internalApiUrl`, + `adminConsole.ingress.path`, `adminConsole.ingress.pathType`, + `adminConsole.ingress.extraHosts`, `adminConsole.route.matches`, + `adminConsole.route.filters` and `server.trustedOriginsAppendAdminConsole` are + removed: console prefixes and rewrites are fixed and the strict schema rejects + the old keys. All roles share `server.publicUrl`; split console server-side + requests use the generated in-cluster `INTERNAL_URL`. - The configuration file is now `authup.yml`, mounted at `/etc/authup/authup.yml`. Provisioning moves to `/etc/authup/provisioning` and - logs to `/var/log/authup`. Remove overrides for `WRITABLE_DIRECTORY_PATH`, + logs to `/var/log/authup`. Remove overrides for `WRITABLE_DIRECTORY_PATH` + (`server.config.WRITABLE_DIRECTORY_PATH` now fails the render), `/var/lib/authup`, or `authup.server.core.conf`. - Split consoles share the Authup origin under `/console/auth`, `/console/admin` and `/console/account`. The generated Ingress rules require diff --git a/charts/authup/README.md b/charts/authup/README.md index 7470e0d..d75b20a 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -126,7 +126,7 @@ Notable operational facts (enforced or warned about by the chart): ## Theming the served consoles -Both consoles server-core serves (the auth pages and `/account`) are rebranded +The served consoles (the auth pages, `/console/admin` and `/console/account`) are rebranded from a directory the chart mounts read-only. Set the manifest as values and the chart composes `theme.json` for you; `files` carries the assets it references: @@ -197,7 +197,7 @@ Kubernetes: `>=1.25.0-0` |-----|------|---------|-------------| | accountConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | | accountConsole.args | list | `[]` | Override the container args | -| accountConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | +| accountConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the account console | | accountConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | | accountConsole.autoscaling.hpa.minReplicas | int | `2` | Minimum replicas | | accountConsole.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | @@ -217,10 +217,9 @@ Kubernetes: `>=1.25.0-0` | accountConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | | accountConsole.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | | accountConsole.hostAliases | list | `[]` | Pod host aliases | -| accountConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | +| accountConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) | | accountConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | | accountConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split account console (requires server.ingress.enabled) | -| accountConsole.ingress.extraHosts | list | `[]` | Extra hosts | | accountConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | accountConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | accountConsole.ingress.extraTls | list | `[]` | Extra TLS entries | @@ -237,13 +236,13 @@ Kubernetes: `>=1.25.0-0` | accountConsole.livenessProbe.timeoutSeconds | int | `5` | | | accountConsole.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere | | accountConsole.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | -| accountConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the UI | +| accountConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the account console | | accountConsole.networkPolicy.extraEgress | list | `[]` | Extra egress rules | | accountConsole.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | | accountConsole.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | | accountConsole.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | | accountConsole.nodeSelector | object | `{}` | Node selector | -| accountConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the UI | +| accountConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the account console | | accountConsole.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | | accountConsole.pdb.minAvailable | string | `""` | Minimum available pods | | accountConsole.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | @@ -289,7 +288,7 @@ Kubernetes: `>=1.25.0-0` | accountConsole.updateStrategy | object | `{"type":"RollingUpdate"}` | Deployment update strategy | | adminConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | | adminConsole.args | list | `[]` | Override the container args | -| adminConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | +| adminConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the admin console | | adminConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | | adminConsole.autoscaling.hpa.minReplicas | int | `2` | Minimum replicas | | adminConsole.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | @@ -309,10 +308,9 @@ Kubernetes: `>=1.25.0-0` | adminConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | | adminConsole.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | | adminConsole.hostAliases | list | `[]` | Pod host aliases | -| adminConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | +| adminConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) | | adminConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | | adminConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split admin console (requires server.ingress.enabled) | -| adminConsole.ingress.extraHosts | list | `[]` | Extra hosts | | adminConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | adminConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | adminConsole.ingress.extraTls | list | `[]` | Extra TLS entries | @@ -329,13 +327,13 @@ Kubernetes: `>=1.25.0-0` | adminConsole.livenessProbe.timeoutSeconds | int | `5` | | | adminConsole.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere | | adminConsole.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | -| adminConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the UI | +| adminConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the admin console | | adminConsole.networkPolicy.extraEgress | list | `[]` | Extra egress rules | | adminConsole.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | | adminConsole.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | | adminConsole.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | | adminConsole.nodeSelector | object | `{}` | Node selector | -| adminConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the UI | +| adminConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the admin console | | adminConsole.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | | adminConsole.pdb.minAvailable | string | `""` | Minimum available pods | | adminConsole.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | @@ -392,7 +390,7 @@ Kubernetes: `>=1.25.0-0` | auth.systemClientSecretReset | bool | `false` | Re-assert the system client secret on every boot (CLIENT_SYSTEM_SECRET_RESET) | | authConsole.affinity | object | `{}` | Affinity (overrides the anti-affinity preset when set) | | authConsole.args | list | `[]` | Override the container args | -| authConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the UI | +| authConsole.autoscaling.hpa.enabled | bool | `false` | Enable HPA for the auth console | | authConsole.autoscaling.hpa.maxReplicas | int | `5` | Maximum replicas | | authConsole.autoscaling.hpa.minReplicas | int | `2` | Minimum replicas | | authConsole.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | @@ -405,17 +403,16 @@ Kubernetes: `>=1.25.0-0` | authConsole.customReadinessProbe | object | `{}` | Custom readiness probe | | authConsole.customStartupProbe | object | `{}` | Custom startup probe | | authConsole.disableRestartOnChanges | bool | `false` | Disable the checksum annotations that roll pods on config changes | -| authConsole.enabled | bool | `true` | Enable the auth console and deploy it separately in split-console mode | +| authConsole.enabled | bool | `true` | Deploy the auth console as its own workload in split-console mode. Must stay true: the auth console owns login and cannot be disabled (ignored in combined mode) | | authConsole.extraEnvVars | list | `[]` | Extra environment variables for the auth console container | | authConsole.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | | authConsole.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | | authConsole.extraVolumeMounts | list | `[]` | Extra volume mounts (tpl-rendered) | | authConsole.extraVolumes | list | `[]` | Extra volumes (tpl-rendered) | | authConsole.hostAliases | list | `[]` | Pod host aliases | -| authConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered) | +| authConsole.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) | | authConsole.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | | authConsole.ingress.enabled | bool | `false` | Enable ingress-nginx routing for the split auth console (requires server.ingress.enabled) | -| authConsole.ingress.extraHosts | list | `[]` | Extra hosts | | authConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | authConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | | authConsole.ingress.extraTls | list | `[]` | Extra TLS entries | @@ -432,13 +429,13 @@ Kubernetes: `>=1.25.0-0` | authConsole.livenessProbe.timeoutSeconds | int | `5` | | | authConsole.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere | | authConsole.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | -| authConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the UI | +| authConsole.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the auth console | | authConsole.networkPolicy.extraEgress | list | `[]` | Extra egress rules | | authConsole.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | | authConsole.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | | authConsole.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | | authConsole.nodeSelector | object | `{}` | Node selector | -| authConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the UI | +| authConsole.pdb.create | bool | `false` | Create a PodDisruptionBudget for the auth console | | authConsole.pdb.maxUnavailable | string | `""` | Maximum unavailable pods (defaults to 1 when both are empty) | | authConsole.pdb.minAvailable | string | `""` | Minimum available pods | | authConsole.podAnnotations | object | `{}` | Pod annotations (tpl-rendered) | @@ -560,7 +557,7 @@ Kubernetes: `>=1.25.0-0` | server.autoscaling.hpa.targetCPU | int | `75` | Target CPU utilization percentage | | server.autoscaling.hpa.targetMemory | string | `""` | Target memory utilization percentage | | 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.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). In split-console mode set them in authConsole.config / accountConsole.config: `start core` does not read console paths | | server.configuration | string | `""` | Content of authup.yml mounted at /etc/authup/authup.yml 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 writable log and npm cache paths; the chart mounts emptyDirs at /var/log/authup and /tmp. | | server.customLivenessProbe | object | `{}` | Custom liveness probe | @@ -581,7 +578,7 @@ Kubernetes: `>=1.25.0-0` | server.hostAliases | list | `[]` | Pod host aliases | | server.ingress.annotations | object | `{}` | Ingress annotations (tpl-rendered). Token responses are large; with ingress-nginx consider proxy-buffer-size 16k+. | | server.ingress.certManager | bool | `false` | Request a cert-manager certificate (adds kubernetes.io/tls-acme) | -| server.ingress.enabled | bool | `false` | Enable ingress for server-core. NOTE: this also exposes the UNAUTHENTICATED /metrics endpoint publicly — block it at the ingress controller or disable it via server.configuration ("middlewarePrometheus: false") when it is not scraped | +| server.ingress.enabled | bool | `false` | Enable ingress for server-core. NOTE: this also exposes the UNAUTHENTICATED /metrics endpoint publicly — block it at the ingress controller or disable it via server.configuration (core.middlewarePrometheus: false) when it is not scraped | | server.ingress.extraHosts | list | `[]` | Extra hosts | | server.ingress.extraPaths | list | `[]` | Extra paths for the primary host | | server.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) | @@ -612,14 +609,14 @@ 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. | +| 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.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) | -| server.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere. When false, only same-namespace pods, the release's UI pods and the configured selectors may connect — add your ingress controller via ingressNSMatchLabels/ingressPodMatchLabels | +| server.networkPolicy.allowExternal | bool | `true` | Allow ingress from anywhere. When false, only the split console pods and the configured selectors may connect: add your ingress controller via ingressNSMatchLabels, ingressPodMatchLabels or extraIngress (required) | | server.networkPolicy.allowExternalEgress | bool | `true` | Allow all egress | -| server.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for server-core | -| server.networkPolicy.extraEgress | list | `[]` | Extra egress rules | +| server.networkPolicy.enabled | bool | `false` | Create a NetworkPolicy for the server and, with server.migration.enabled, a hook-scoped egress policy for the migration Job | +| server.networkPolicy.extraEgress | list | `[]` | Extra egress rules (also applied to the migration Job policy) | | server.networkPolicy.extraIngress | list | `[]` | Extra ingress rules | | server.networkPolicy.ingressNSMatchLabels | object | `{}` | Namespace labels allowed to connect when allowExternal is false | | server.networkPolicy.ingressPodMatchLabels | object | `{}` | Pod labels allowed to connect when allowExternal is false | @@ -673,7 +670,7 @@ 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. Experimental upstream: the directory layout and the theme* options may change in a minor release | +| server.theme.enabled | bool | `false` | Mount an operator theme for the served consoles (auth, admin and account). Experimental upstream: the directory layout and the theme* options may change in a minor release | | server.theme.existingConfigMap | string | `""` | Existing ConfigMap holding the theme (tpl-rendered name). Use for binary assets, which cannot be expressed in files. Mounted whole, so it must carry theme.json itself and excludes the manifest values above | | 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.favicon | string | `""` | Favicon path, relative to the theme root and under assets/ (e.g. assets/favicon.svg). Must be a key of files | @@ -682,7 +679,7 @@ Kubernetes: `>=1.25.0-0` | server.theme.logo | string | `""` | Logo replacing the built-in mark on both consoles, under assets/. Painted into the existing mark's box, so it needs no sizing | | server.theme.logoDark | string | `""` | Dark-mode logo variant, under assets/. Without it dark mode reuses logo, which disappears when the mark is drawn dark-on-light | | server.theme.stylesheet | string | `""` | Stylesheet path, under assets/ and ending in .css. Linked last, so it beats the token block; it is unlayered, so set dark colors explicitly | -| server.theme.title | string | `""` | Document title of both served consoles ("" = authup's own) | +| server.theme.title | string | `""` | Document title of the served consoles ("" = authup's own) | | server.theme.tokens | object | `{}` | authup-periwinkle alone recolors buttons, focus rings and links. A color set here also wins in dark mode: put surface colors in both tokens and tokensDark | | server.theme.tokensDark | object | `{}` | CSS custom properties applied in dark mode only (tpl-rendered) | | server.tolerations | list | `[]` | Tolerations | @@ -723,7 +720,7 @@ Kubernetes: `>=1.25.0-0` | worker.command | list | `[]` | Override the container command | | worker.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context | | worker.disableRestartOnChanges | bool | `false` | Disable checksum annotations that roll pods on configuration changes | -| worker.enabled | bool | `false` | Deploy a dedicated background worker (requires server.enabled) | +| worker.enabled | bool | `false` | Deploy a dedicated background worker (requires server.enabled). It never migrates: on a fresh install it restarts until the server has initialized the schema; enable server.migration.enabled so upgrades run the Job first | | worker.extraEnvVars | list | `[]` | Extra environment variables for the worker container | | worker.extraEnvVarsCM | string | `""` | Extra ConfigMap with environment variables (tpl-rendered name) | | worker.extraEnvVarsSecret | string | `""` | Extra Secret with environment variables (tpl-rendered name) | diff --git a/charts/authup/README.md.gotmpl b/charts/authup/README.md.gotmpl index 9a13ae2..b879d95 100644 --- a/charts/authup/README.md.gotmpl +++ b/charts/authup/README.md.gotmpl @@ -139,7 +139,7 @@ Notable operational facts (enforced or warned about by the chart): ## Theming the served consoles -Both consoles server-core serves (the auth pages and `/account`) are rebranded +The served consoles (the auth pages, `/console/admin` and `/console/account`) are rebranded from a directory the chart mounts read-only. Set the manifest as values and the chart composes `theme.json` for you; `files` carries the assets it references: diff --git a/charts/authup/ci/theme-values.yaml b/charts/authup/ci/theme-values.yaml index e28fb13..c3d8135 100644 --- a/charts/authup/ci/theme-values.yaml +++ b/charts/authup/ci/theme-values.yaml @@ -2,8 +2,6 @@ # manifest values (the chart composes theme.json) and the inline files map # for the assets they reference, which exercises 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 diff --git a/charts/authup/ci/valkey-values.yaml b/charts/authup/ci/valkey-values.yaml index d154554..88d3d33 100644 --- a/charts/authup/ci/valkey-values.yaml +++ b/charts/authup/ci/valkey-values.yaml @@ -14,7 +14,8 @@ server: # the hook-scoped copy exists for. Deliberately a no-op option: a db block # here would fight the connection this scenario installs. configuration: | - logger: true + core: + logger: true resources: requests: cpu: 50m diff --git a/charts/authup/templates/NOTES.txt b/charts/authup/templates/NOTES.txt index 955522a..63d3a18 100644 --- a/charts/authup/templates/NOTES.txt +++ b/charts/authup/templates/NOTES.txt @@ -45,7 +45,7 @@ PUBLIC_URL. NOTE: the server ingress also exposes the unauthenticated /metrics endpoint. Block it at the ingress controller or disable it through server.configuration -when it is not scraped. +(core.middlewarePrometheus: false) when it is not scraped. {{- end }} {{- end }} diff --git a/charts/authup/templates/_console-env.tpl b/charts/authup/templates/_console-env.tpl index 37afae9..8fe7ac8 100644 --- a/charts/authup/templates/_console-env.tpl +++ b/charts/authup/templates/_console-env.tpl @@ -8,16 +8,20 @@ PUBLIC_URL: {{ $publicUrl | quote }} {{- end }} INTERNAL_URL: {{ printf "http://%s:%v" (include "authup.server.fullname" $ctx) $ctx.Values.server.service.ports.http | quote }} +{{- $trustedOrigins := include "authup.server.trustedOrigins" $ctx }} +{{- if $trustedOrigins }} +TRUSTED_ORIGINS: {{ $trustedOrigins | quote }} +{{- end }} ADMIN_CONSOLE_ENABLED: {{ $ctx.Values.adminConsole.enabled | toString | quote }} ACCOUNT_CONSOLE_ENABLED: {{ $ctx.Values.accountConsole.enabled | toString | quote }} {{ $portName }}: {{ $values.containerPorts.http | toString | quote }} {{- if include "authup.server.themeMounted" $ctx }} {{ include "authup.server.themeEnv" $ctx }} {{- end }} -{{- $reserved := list "PUBLIC_URL" "INTERNAL_URL" "ADMIN_CONSOLE_ENABLED" "ACCOUNT_CONSOLE_ENABLED" $portName "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" }} +{{- $reserved := list "PUBLIC_URL" "INTERNAL_URL" "TRUSTED_ORIGINS" "ADMIN_CONSOLE_ENABLED" "ACCOUNT_CONSOLE_ENABLED" $portName "THEME_DIRECTORY_PATH" "THEME_FRAGMENTS_ENABLED" }} {{- range $key, $value := $values.config }} {{- if has $key $reserved }} -{{- fail (printf "authup: %s.config.%s collides with a first-class chart value; set the dedicated value instead." .key $key) }} +{{- fail (printf "authup: %s.config.%s collides with a first-class chart value; set the dedicated value instead." $.key $key) }} {{- end }} {{ $key }}: {{ include "authup.tplvalues.render" (dict "value" ($value | toString) "context" $ctx) | quote }} {{- end }} diff --git a/charts/authup/templates/_ingress.tpl b/charts/authup/templates/_ingress.tpl index c323b53..8a4831a 100644 --- a/charts/authup/templates/_ingress.tpl +++ b/charts/authup/templates/_ingress.tpl @@ -74,7 +74,7 @@ spec: - {{ $hostname | quote }} {{- /* Derived from the component name, not the hostname: a wildcard host would produce an invalid Secret name, and a shared hostname across - server + ui would make two ingresses fight over one secret. */}} + server + console would make two ingresses fight over one secret. */}} secretName: {{ printf "%s-tls" .name | quote }} {{- end }} {{- if $ing.extraTls }} diff --git a/charts/authup/templates/account-console/ingress.yaml b/charts/authup/templates/account-console/ingress.yaml index 810dd9d..9f02489 100644 --- a/charts/authup/templates/account-console/ingress.yaml +++ b/charts/authup/templates/account-console/ingress.yaml @@ -9,8 +9,11 @@ metadata: labels: {{- include "authup.labels" (dict "context" $ "component" "account-console") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} - {{- if $ingress.annotations }} - {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- with omit ($ingress.annotations | default dict) "nginx.ingress.kubernetes.io/use-regex" "nginx.ingress.kubernetes.io/rewrite-target" }} + {{- include "authup.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- if $ingress.certManager }} + kubernetes.io/tls-acme: "true" {{- end }} nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: /$2 diff --git a/charts/authup/templates/admin-console/ingress.yaml b/charts/authup/templates/admin-console/ingress.yaml index e31e77d..12c285c 100644 --- a/charts/authup/templates/admin-console/ingress.yaml +++ b/charts/authup/templates/admin-console/ingress.yaml @@ -9,8 +9,11 @@ metadata: labels: {{- include "authup.labels" (dict "context" $ "component" "admin-console") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} - {{- if $ingress.annotations }} - {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- with omit ($ingress.annotations | default dict) "nginx.ingress.kubernetes.io/use-regex" "nginx.ingress.kubernetes.io/rewrite-target" }} + {{- include "authup.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- if $ingress.certManager }} + kubernetes.io/tls-acme: "true" {{- end }} nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: /$2 diff --git a/charts/authup/templates/auth-console/ingress.yaml b/charts/authup/templates/auth-console/ingress.yaml index 0b00bcf..041cf55 100644 --- a/charts/authup/templates/auth-console/ingress.yaml +++ b/charts/authup/templates/auth-console/ingress.yaml @@ -9,8 +9,11 @@ metadata: labels: {{- include "authup.labels" (dict "context" $ "component" "auth-console") | nindent 4 }} annotations: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} - {{- if $ingress.annotations }} - {{- include "authup.tplvalues.render" (dict "value" $ingress.annotations "context" $) | nindent 4 }} + {{- with omit ($ingress.annotations | default dict) "nginx.ingress.kubernetes.io/use-regex" "nginx.ingress.kubernetes.io/rewrite-target" }} + {{- include "authup.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- if $ingress.certManager }} + kubernetes.io/tls-acme: "true" {{- end }} nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: /$2 diff --git a/charts/authup/templates/server/configmap-env.yaml b/charts/authup/templates/server/configmap-env.yaml index 27176cd..8606268 100644 --- a/charts/authup/templates/server/configmap-env.yaml +++ b/charts/authup/templates/server/configmap-env.yaml @@ -7,7 +7,7 @@ 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 + {{- /* The server and worker Deployments consume 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" . }} diff --git a/charts/authup/templates/server/deployment.yaml b/charts/authup/templates/server/deployment.yaml index dee869f..d886583 100644 --- a/charts/authup/templates/server/deployment.yaml +++ b/charts/authup/templates/server/deployment.yaml @@ -128,7 +128,11 @@ spec: - name: WORKER_ENABLED value: "false" {{- end }} - {{- if and .Values.server.migration.enabled .Release.IsUpgrade (or (not (or .Values.postgresql.enabled .Values.mysql.enabled)) (and .Values.postgresql.enabled .Values.postgresql.persistence.enabled) (and .Values.mysql.enabled .Values.mysql.persistence.enabled)) }} + {{- /* 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. */}} + {{- $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 value: "false" {{- end }} diff --git a/charts/authup/templates/server/migration-networkpolicy.yaml b/charts/authup/templates/server/migration-networkpolicy.yaml index 90a2ab8..9fb0010 100644 --- a/charts/authup/templates/server/migration-networkpolicy.yaml +++ b/charts/authup/templates/server/migration-networkpolicy.yaml @@ -9,7 +9,9 @@ metadata: {{- include "authup.annotations" (dict "context" $) | nindent 4 }} {{- if .Values.useHelmHooks }} helm.sh/hook: pre-upgrade - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + {{- /* No hook-succeeded: Helm 4 deletes earlier succeeded hooks as soon as a + later hook fails, which would cut a still-running migration pod off. */}} + helm.sh/hook-delete-policy: before-hook-creation helm.sh/hook-weight: "-5" {{- else }} argocd.argoproj.io/hook: PreSync diff --git a/charts/authup/templates/server/networkpolicy.yaml b/charts/authup/templates/server/networkpolicy.yaml index 52db1e7..651b06c 100644 --- a/charts/authup/templates/server/networkpolicy.yaml +++ b/charts/authup/templates/server/networkpolicy.yaml @@ -13,12 +13,14 @@ spec: - Ingress - Egress ingress: + {{- /* A rule whose `from` list is empty admits every source, so the port rule + only renders when it has at least one peer; extraIngress can stand alone. */}} + {{- if or .Values.server.networkPolicy.allowExternal .Values.server.splitConsoles .Values.server.networkPolicy.ingressPodMatchLabels .Values.server.networkPolicy.ingressNSMatchLabels }} - ports: - port: 3000 protocol: TCP {{- if not .Values.server.networkPolicy.allowExternal }} from: - - podSelector: {} {{- if .Values.server.splitConsoles }} {{- if .Values.authConsole.enabled }} - podSelector: @@ -42,6 +44,7 @@ spec: matchLabels: {{- include "authup.tplvalues.render" (dict "value" .Values.server.networkPolicy.ingressNSMatchLabels "context" $) | nindent 14 }} {{- end }} {{- end }} + {{- end }} {{- if .Values.server.networkPolicy.extraIngress }} {{- include "authup.tplvalues.render" (dict "value" .Values.server.networkPolicy.extraIngress "context" $) | nindent 4 }} {{- end }} diff --git a/charts/authup/templates/validations.yaml b/charts/authup/templates/validations.yaml index e1eb181..766b89b 100644 --- a/charts/authup/templates/validations.yaml +++ b/charts/authup/templates/validations.yaml @@ -57,6 +57,9 @@ database is configured while the mounted file is the operator's. {{- end }} {{- if .Values.server.splitConsoles }} {{- $publicUrl := include "authup.server.publicUrl" . }} +{{- if not $publicUrl }} +{{- fail "authup: server.splitConsoles requires server.publicUrl or an enabled server.ingress with a hostname so every role shares one origin." }} +{{- end }} {{- $path := (urlParse $publicUrl).path }} {{- if and $path (ne $path "/") }} {{- fail "authup: server.splitConsoles requires server.publicUrl at the origin root because the console routes use fixed /console/* paths." }} @@ -89,6 +92,10 @@ database is configured while the mounted file is the operator's. {{- end }} {{- end }} {{- end }} +{{/* Browser traffic reaches the API through the ingress controller; console peers do not cover it. */}} +{{- if and .Values.server.enabled .Values.server.networkPolicy.enabled (not .Values.server.networkPolicy.allowExternal) (not .Values.server.networkPolicy.ingressPodMatchLabels) (not .Values.server.networkPolicy.ingressNSMatchLabels) (not .Values.server.networkPolicy.extraIngress) }} +{{- fail "authup: server.networkPolicy.allowExternal=false needs an ingress source for browser traffic (ingressNSMatchLabels, ingressPodMatchLabels or extraIngress); split console peers do not cover it." }} +{{- end }} {{- if and .Values.worker.enabled (not .Values.server.enabled) }} {{- fail "authup: worker.enabled requires server.enabled=true because the worker shares the server configuration and credentials." }} {{- end }} @@ -107,14 +114,14 @@ database is configured while the mounted file is the operator's. {{- if and .Values.server.ingress.enabled (not .Values.server.ingress.hostname) (not .Values.server.ingress.extraHosts) (not .Values.server.ingress.extraRules) }} {{- fail "authup: server.ingress.enabled requires server.ingress.hostname (or extraHosts / extraRules)." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled (not .Values.authConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.authConsole.ingress.extraHosts) (not .Values.authConsole.ingress.extraRules) }} -{{- fail "authup: authConsole.ingress.enabled requires authConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} +{{- if and .Values.server.splitConsoles .Values.authConsole.enabled .Values.authConsole.ingress.enabled (not .Values.authConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.authConsole.ingress.extraRules) }} +{{- fail "authup: authConsole.ingress.enabled requires authConsole.ingress.hostname, server.ingress.hostname, or extraRules." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.adminConsole.ingress.extraHosts) (not .Values.adminConsole.ingress.extraRules) }} -{{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} +{{- if and .Values.server.splitConsoles .Values.adminConsole.enabled .Values.adminConsole.ingress.enabled (not .Values.adminConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.adminConsole.ingress.extraRules) }} +{{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname, server.ingress.hostname, or extraRules." }} {{- end }} -{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled (not .Values.accountConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.accountConsole.ingress.extraHosts) (not .Values.accountConsole.ingress.extraRules) }} -{{- fail "authup: accountConsole.ingress.enabled requires accountConsole.ingress.hostname, server.ingress.hostname, or extraHosts / extraRules." }} +{{- if and .Values.server.splitConsoles .Values.accountConsole.enabled .Values.accountConsole.ingress.enabled (not .Values.accountConsole.ingress.hostname) (not .Values.server.ingress.hostname) (not .Values.accountConsole.ingress.extraRules) }} +{{- fail "authup: accountConsole.ingress.enabled requires accountConsole.ingress.hostname, server.ingress.hostname, or extraRules." }} {{- end }} {{/* @@ -158,3 +165,6 @@ Deprecation tripwires: when a value moves, add a guard here that names the new location, e.g. if .Values. -> fail "authup: `` moved to `server.` in chart 0.X. See BREAKING.md." */}} +{{- if and .Values.server.config (hasKey .Values.server.config "WRITABLE_DIRECTORY_PATH") }} +{{- fail "authup: server.config.WRITABLE_DIRECTORY_PATH is retired in beta.64: logs live at /var/log/authup and provisioning at /etc/authup/provisioning. See BREAKING.md." }} +{{- end }} diff --git a/charts/authup/templates/worker/deployment.yaml b/charts/authup/templates/worker/deployment.yaml index 1f87dcf..44c34a3 100644 --- a/charts/authup/templates/worker/deployment.yaml +++ b/charts/authup/templates/worker/deployment.yaml @@ -25,6 +25,12 @@ spec: annotations: {{- if not .Values.worker.disableRestartOnChanges }} checksum/env: {{ include "authup.server.configEnv" . | sha256sum }} + {{- if and (not .Values.postgresql.enabled) (not .Values.mysql.enabled) .Values.externalDatabase.host .Values.externalDatabase.password (not .Values.externalDatabase.existingSecret) }} + checksum/secret-db: {{ include (print $.Template.BasePath "/secret-db.yaml") . | sha256sum }} + {{- end }} + {{- if and (not .Values.valkey.enabled) (not .Values.externalRedis.existingSecret) (or .Values.externalRedis.url .Values.externalRedis.host) }} + checksum/secret-redis: {{ include (print $.Template.BasePath "/secret-redis.yaml") . | sha256sum }} + {{- end }} {{- if .Values.server.configuration }} checksum/configuration: {{ include (print $.Template.BasePath "/server/configmap-configuration.yaml") . | sha256sum }} {{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index e502c7d..0ea01f6 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -28,7 +28,7 @@ "properties": { "enabled": { "default": false, - "description": "Enable HPA for the UI", + "description": "Enable HPA for the account console", "required": [], "title": "enabled", "type": "boolean" @@ -284,7 +284,7 @@ "properties": { "annotations": { "additionalProperties": true, - "description": "Ingress annotations (tpl-rendered)", + "description": "Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned)", "required": [], "title": "annotations" }, @@ -302,15 +302,6 @@ "title": "enabled", "type": "boolean" }, - "extraHosts": { - "description": "Extra hosts", - "items": { - "required": [] - }, - "required": [], - "title": "extraHosts", - "type": "array" - }, "extraPaths": { "description": "Extra paths for the primary host", "items": { @@ -366,7 +357,6 @@ "ingressClassName", "tls", "certManager", - "extraHosts", "extraPaths", "extraTls", "extraRules" @@ -460,7 +450,7 @@ }, "enabled": { "default": false, - "description": "Create a NetworkPolicy for the UI", + "description": "Create a NetworkPolicy for the account console", "required": [], "title": "enabled", "type": "boolean" @@ -517,7 +507,7 @@ "properties": { "create": { "default": false, - "description": "Create a PodDisruptionBudget for the UI", + "description": "Create a PodDisruptionBudget for the account console", "required": [], "title": "create", "type": "boolean" @@ -1039,7 +1029,7 @@ "properties": { "enabled": { "default": false, - "description": "Enable HPA for the UI", + "description": "Enable HPA for the admin console", "required": [], "title": "enabled", "type": "boolean" @@ -1295,7 +1285,7 @@ "properties": { "annotations": { "additionalProperties": true, - "description": "Ingress annotations (tpl-rendered)", + "description": "Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned)", "required": [], "title": "annotations" }, @@ -1313,15 +1303,6 @@ "title": "enabled", "type": "boolean" }, - "extraHosts": { - "description": "Extra hosts", - "items": { - "required": [] - }, - "required": [], - "title": "extraHosts", - "type": "array" - }, "extraPaths": { "description": "Extra paths for the primary host", "items": { @@ -1377,7 +1358,6 @@ "ingressClassName", "tls", "certManager", - "extraHosts", "extraPaths", "extraTls", "extraRules" @@ -1471,7 +1451,7 @@ }, "enabled": { "default": false, - "description": "Create a NetworkPolicy for the UI", + "description": "Create a NetworkPolicy for the admin console", "required": [], "title": "enabled", "type": "boolean" @@ -1528,7 +1508,7 @@ "properties": { "create": { "default": false, - "description": "Create a PodDisruptionBudget for the UI", + "description": "Create a PodDisruptionBudget for the admin console", "required": [], "title": "create", "type": "boolean" @@ -2159,7 +2139,7 @@ "properties": { "enabled": { "default": false, - "description": "Enable HPA for the UI", + "description": "Enable HPA for the auth console", "required": [], "title": "enabled", "type": "boolean" @@ -2355,7 +2335,7 @@ }, "enabled": { "default": true, - "description": "Enable the auth console and deploy it separately in split-console mode", + "description": "Deploy the auth console as its own workload in split-console mode. Must\nstay true: the auth console owns login and cannot be disabled (ignored in\ncombined mode)", "required": [], "title": "enabled", "type": "boolean" @@ -2415,7 +2395,7 @@ "properties": { "annotations": { "additionalProperties": true, - "description": "Ingress annotations (tpl-rendered)", + "description": "Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned)", "required": [], "title": "annotations" }, @@ -2433,15 +2413,6 @@ "title": "enabled", "type": "boolean" }, - "extraHosts": { - "description": "Extra hosts", - "items": { - "required": [] - }, - "required": [], - "title": "extraHosts", - "type": "array" - }, "extraPaths": { "description": "Extra paths for the primary host", "items": { @@ -2497,7 +2468,6 @@ "ingressClassName", "tls", "certManager", - "extraHosts", "extraPaths", "extraTls", "extraRules" @@ -2591,7 +2561,7 @@ }, "enabled": { "default": false, - "description": "Create a NetworkPolicy for the UI", + "description": "Create a NetworkPolicy for the auth console", "required": [], "title": "enabled", "type": "boolean" @@ -2648,7 +2618,7 @@ "properties": { "create": { "default": false, - "description": "Create a PodDisruptionBudget for the UI", + "description": "Create a PodDisruptionBudget for the auth console", "required": [], "title": "create", "type": "boolean" @@ -4203,7 +4173,7 @@ }, "config": { "additionalProperties": true, - "description": "Extra environment variables rendered literally into the env ConfigMap\n(map of NAME: value) for options without first-class values, e.g.\nAUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console\nwith your own build (pair them with extraVolumes; the substituted package\nowns the login flow, so use server.theme for branding instead)", + "description": "Extra environment variables rendered literally into the env ConfigMap\n(map of NAME: value) for options without first-class values, e.g.\nAUTH_CONSOLE_PATH / ACCOUNT_CONSOLE_PATH, which replace a served console\nwith your own build (pair them with extraVolumes; the substituted package\nowns the login flow, so use server.theme for branding instead). In\nsplit-console mode set them in authConsole.config / accountConsole.config:\n`start core` does not read console paths", "required": [], "title": "config" }, @@ -4451,7 +4421,7 @@ }, "enabled": { "default": false, - "description": "Enable ingress for server-core. NOTE: this also exposes the\nUNAUTHENTICATED /metrics endpoint publicly — block it at the ingress\ncontroller or disable it via server.configuration\n(\"middlewarePrometheus: false\") when it is not scraped", + "description": "Enable ingress for server-core. NOTE: this also exposes the\nUNAUTHENTICATED /metrics endpoint publicly — block it at the ingress\ncontroller or disable it via server.configuration\n(core.middlewarePrometheus: false) when it is not scraped", "required": [], "title": "enabled", "type": "boolean" @@ -4742,7 +4712,7 @@ }, "enabled": { "default": false, - "description": "Run `migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls and non-persistent built-in databases migrate at boot regardless.", + "description": "Run `migration run` as a pre-upgrade hook Job. Recommended\nfor multi-replica deployments (serializes DDL before pods roll). Fresh\ninstalls and non-persistent built-in databases migrate at boot regardless.\nUnder useHelmHooks=false the Job is a PreSync hook on the first sync too,\nso with a built-in database enable it only after that sync", "required": [], "title": "enabled", "type": "boolean" @@ -4780,7 +4750,7 @@ "properties": { "allowExternal": { "default": true, - "description": "Allow ingress from anywhere. When false, only same-namespace pods,\nthe release's UI pods and the configured selectors may connect — add\nyour ingress controller via ingressNSMatchLabels/ingressPodMatchLabels", + "description": "Allow ingress from anywhere. When false, only the split console pods\nand the configured selectors may connect: add your ingress controller via\ningressNSMatchLabels, ingressPodMatchLabels or extraIngress (required)", "required": [], "title": "allowExternal", "type": "boolean" @@ -4794,13 +4764,13 @@ }, "enabled": { "default": false, - "description": "Create a NetworkPolicy for server-core", + "description": "Create a NetworkPolicy for the server and, with server.migration.enabled,\na hook-scoped egress policy for the migration Job", "required": [], "title": "enabled", "type": "boolean" }, "extraEgress": { - "description": "Extra egress rules", + "description": "Extra egress rules (also applied to the migration Job policy)", "items": { "required": [] }, @@ -5355,7 +5325,7 @@ "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. Experimental upstream: the\ndirectory layout and the theme* options may change in a minor release", + "description": "Mount an operator theme for the served consoles (auth, admin and\naccount). Experimental upstream: the directory layout and the theme*\noptions may change in a minor release", "required": [], "title": "enabled", "type": "boolean" @@ -5419,7 +5389,7 @@ }, "title": { "default": "", - "description": "Document title of both served consoles (\"\" = authup's own)", + "description": "Document title of the served consoles (\"\" = authup's own)", "required": [], "title": "title", "type": "string" @@ -6099,7 +6069,7 @@ }, "enabled": { "default": false, - "description": "Deploy a dedicated background worker (requires server.enabled)", + "description": "Deploy a dedicated background worker (requires server.enabled). It never\nmigrates: on a fresh install it restarts until the server has initialized\nthe schema; enable server.migration.enabled so upgrades run the Job first", "required": [], "title": "enabled", "type": "boolean" diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index 66051d4..8234fd2 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -448,7 +448,9 @@ server: # (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) + # owns the login flow, so use server.theme for branding instead). In + # split-console mode set them in authConsole.config / accountConsole.config: + # `start core` does not read console paths config: {} # -- Content of authup.yml mounted at /etc/authup/authup.yml for file-only # options (middleware objects, per-field SMTP, CORS allowlist). Environment @@ -472,12 +474,11 @@ server: # 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. Experimental upstream: the - # directory layout and the theme* options may change in a minor release + # -- Mount an operator theme for the served consoles (auth, admin and + # account). Experimental upstream: the directory layout and the theme* + # options may change in a minor release enabled: false - # -- Document title of both served consoles ("" = authup's own) + # -- Document title of the served consoles ("" = authup's own) title: "" # -- Favicon path, relative to the theme root and under assets/ # (e.g. assets/favicon.svg). Must be a key of files @@ -528,6 +529,8 @@ server: # -- 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 enabled: false # -- Job backoff limit backoffLimit: 3 @@ -723,7 +726,7 @@ server: # -- Enable ingress for server-core. NOTE: this also exposes the # UNAUTHENTICATED /metrics endpoint publicly — block it at the ingress # controller or disable it via server.configuration - # ("middlewarePrometheus: false") when it is not scraped + # (core.middlewarePrometheus: false) when it is not scraped enabled: false # -- Ingress hostname (tpl-rendered); also drives the derived PUBLIC_URL hostname: "" @@ -818,11 +821,12 @@ server: # -- Target memory utilization percentage targetMemory: "" networkPolicy: - # -- Create a NetworkPolicy for server-core + # -- Create a NetworkPolicy for the server and, with server.migration.enabled, + # a hook-scoped egress policy for the migration Job enabled: false - # -- Allow ingress from anywhere. When false, only same-namespace pods, - # the release's UI pods and the configured selectors may connect — add - # your ingress controller via ingressNSMatchLabels/ingressPodMatchLabels + # -- Allow ingress from anywhere. When false, only the split console pods + # and the configured selectors may connect: add your ingress controller via + # ingressNSMatchLabels, ingressPodMatchLabels or extraIngress (required) allowExternal: true # -- Allow all egress allowExternalEgress: true @@ -838,13 +842,15 @@ server: ingressNSMatchLabels: {} # -- Extra ingress rules extraIngress: [] - # -- Extra egress rules + # -- Extra egress rules (also applied to the migration Job policy) extraEgress: [] ## @section Split console workloads authConsole: - # -- Enable the auth console and deploy it separately in split-console mode + # -- Deploy the auth console as its own workload in split-console mode. Must + # stay true: the auth console owns login and cannot be disabled (ignored in + # combined mode) enabled: true # -- Number of auth console replicas replicaCount: 1 @@ -1040,14 +1046,12 @@ authConsole: # @schema # additionalProperties: true # @schema - # -- Ingress annotations (tpl-rendered) + # -- Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) annotations: {} # -- Enable TLS for the hostname tls: false # -- Request a cert-manager certificate (adds kubernetes.io/tls-acme) certManager: false - # -- Extra hosts - extraHosts: [] # -- Extra paths for the primary host extraPaths: [] # -- Extra TLS entries @@ -1074,7 +1078,7 @@ authConsole: # -- HTTPRoute annotations annotations: {} pdb: - # -- Create a PodDisruptionBudget for the UI + # -- Create a PodDisruptionBudget for the auth console create: false # -- Minimum available pods minAvailable: "" @@ -1082,7 +1086,7 @@ authConsole: maxUnavailable: "" autoscaling: hpa: - # -- Enable HPA for the UI + # -- Enable HPA for the auth console enabled: false # -- Minimum replicas minReplicas: 2 @@ -1093,7 +1097,7 @@ authConsole: # -- Target memory utilization percentage targetMemory: "" networkPolicy: - # -- Create a NetworkPolicy for the UI + # -- Create a NetworkPolicy for the auth console enabled: false # -- Allow ingress from anywhere allowExternal: true @@ -1312,14 +1316,12 @@ adminConsole: # @schema # additionalProperties: true # @schema - # -- Ingress annotations (tpl-rendered) + # -- Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) annotations: {} # -- Enable TLS for the hostname tls: false # -- Request a cert-manager certificate (adds kubernetes.io/tls-acme) certManager: false - # -- Extra hosts - extraHosts: [] # -- Extra paths for the primary host extraPaths: [] # -- Extra TLS entries @@ -1346,7 +1348,7 @@ adminConsole: # -- HTTPRoute annotations annotations: {} pdb: - # -- Create a PodDisruptionBudget for the UI + # -- Create a PodDisruptionBudget for the admin console create: false # -- Minimum available pods minAvailable: "" @@ -1354,7 +1356,7 @@ adminConsole: maxUnavailable: "" autoscaling: hpa: - # -- Enable HPA for the UI + # -- Enable HPA for the admin console enabled: false # -- Minimum replicas minReplicas: 2 @@ -1365,7 +1367,7 @@ adminConsole: # -- Target memory utilization percentage targetMemory: "" networkPolicy: - # -- Create a NetworkPolicy for the UI + # -- Create a NetworkPolicy for the admin console enabled: false # -- Allow ingress from anywhere allowExternal: true @@ -1584,14 +1586,12 @@ accountConsole: # @schema # additionalProperties: true # @schema - # -- Ingress annotations (tpl-rendered) + # -- Ingress annotations (tpl-rendered; use-regex and rewrite-target are chart-owned) annotations: {} # -- Enable TLS for the hostname tls: false # -- Request a cert-manager certificate (adds kubernetes.io/tls-acme) certManager: false - # -- Extra hosts - extraHosts: [] # -- Extra paths for the primary host extraPaths: [] # -- Extra TLS entries @@ -1618,7 +1618,7 @@ accountConsole: # -- HTTPRoute annotations annotations: {} pdb: - # -- Create a PodDisruptionBudget for the UI + # -- Create a PodDisruptionBudget for the account console create: false # -- Minimum available pods minAvailable: "" @@ -1626,7 +1626,7 @@ accountConsole: maxUnavailable: "" autoscaling: hpa: - # -- Enable HPA for the UI + # -- Enable HPA for the account console enabled: false # -- Minimum replicas minReplicas: 2 @@ -1637,7 +1637,7 @@ accountConsole: # -- Target memory utilization percentage targetMemory: "" networkPolicy: - # -- Create a NetworkPolicy for the UI + # -- Create a NetworkPolicy for the account console enabled: false # -- Allow ingress from anywhere allowExternal: true @@ -1661,7 +1661,9 @@ accountConsole: ## @section Worker worker: - # -- Deploy a dedicated background worker (requires server.enabled) + # -- Deploy a dedicated background worker (requires server.enabled). It never + # migrates: on a fresh install it restarts until the server has initialized + # the schema; enable server.migration.enabled so upgrades run the Job first enabled: false # -- Number of worker replicas (one is normally sufficient) replicaCount: 1 diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 1027422..a72544f 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -219,6 +219,9 @@ def check_base(): migration_upgrade = render(migration_values, "--is-upgrade") migration_server = one(migration_upgrade, "Deployment", "server") assert effective_env(migration_server, migration_upgrade)["MIGRATION_ENABLED"] == "false" + argocd_install = render(migration_values, "--set", "useHelmHooks=false") + argocd_server = one(argocd_install, "Deployment", "server") + assert effective_env(argocd_server, argocd_install)["MIGRATION_ENABLED"] == "false" def check_split(): @@ -269,6 +272,14 @@ def check_split(): assert "CLIENT_SYSTEM_SECRET" not in worker_env assert "MIGRATION_ENABLED" not in worker_env + trusted = render( + chart / "ci" / "split-values.yaml", + "--set", + "server.trustedOrigins={https://app.example.com}", + ) + for component in ("auth-console", "admin-console", "account-console"): + console = one(trusted, "Deployment", component) + assert effective_env(console, trusted)["TRUSTED_ORIGINS"] == "https://app.example.com" upgrade = render(chart / "ci" / "split-values.yaml", "--is-upgrade") upgrade_server = one(upgrade, "Deployment", "server") assert "MIGRATION_ENABLED" not in effective_env(upgrade_server, upgrade) @@ -363,6 +374,7 @@ def check_policy(): annotations = migration["metadata"]["annotations"] assert annotations["helm.sh/hook"] == "pre-upgrade" assert annotations["helm.sh/hook-weight"] == "-5" + assert annotations["helm.sh/hook-delete-policy"] == "before-hook-creation" assert migration["spec"]["policyTypes"] == ["Egress"] worker = one(documents, "NetworkPolicy", "worker") @@ -379,10 +391,7 @@ def check_policy(): "account-console", } <= peer_components(server, "ingress") ingress_peers = server["spec"]["ingress"][0]["from"] - assert any( - peer.get("podSelector") == {} and "namespaceSelector" not in peer - for peer in ingress_peers - ) + assert not any(peer.get("podSelector") == {} for peer in ingress_peers) assert any( peer.get("namespaceSelector", {}).get("matchLabels", {}).get( "kubernetes.io/metadata.name" @@ -418,6 +427,46 @@ def check_validations(): {"server": {"features": {"accountConsole": False}}}, "server.features.accountConsole moved to accountConsole.enabled", ) + render_fails( + {"server": {"config": {"WRITABLE_DIRECTORY_PATH": "/x"}}}, + "server.config.WRITABLE_DIRECTORY_PATH is retired", + ) + render_fails( + {"server": {"splitConsoles": True}}, + "server.splitConsoles requires server.publicUrl or an enabled server.ingress", + ) + render_fails( + { + "server": { + "publicUrl": "https://auth.example.com", + "splitConsoles": True, + }, + "authConsole": {"config": {"PUBLIC_URL": "https://x.example.com"}}, + }, + "authConsole.config.PUBLIC_URL collides with a first-class chart value", + ) + render_fails( + {"server": {"networkPolicy": {"enabled": True, "allowExternal": False}}}, + "server.networkPolicy.allowExternal=false needs an ingress source", + ) + extra_only = render( + { + "server": { + "networkPolicy": { + "enabled": True, + "allowExternal": False, + "extraIngress": [{"from": [{"namespaceSelector": {"matchLabels": {"team": "ingress"}}}]}], + } + } + } + ) + policy = one(extra_only, "NetworkPolicy", "server") + assert policy["spec"]["ingress"] == [{"from": [{"namespaceSelector": {"matchLabels": {"team": "ingress"}}}]}] + render( + chart / "ci" / "split-values.yaml", + "--set", + "authConsole.ingress.annotations=null", + ) render_fails( { "server": { From 4c18a697d3942f5f3943e8d7cf132a04ea132d21 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 15:08:10 +0200 Subject: [PATCH 13/14] ci: install the contract check's PyYAML through setup-python The uv pip install only worked because chart-testing-action activates a venv and puts uv on PATH; a dedicated Python step removes that coupling. --- .github/workflows/lint-test.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index 2616f77..48fd7b7 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -47,9 +47,14 @@ jobs: - name: Check values coverage run: make lint-values-coverage + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Check Authup beta.64 contract run: | - uv pip install PyYAML==6.0.2 + pip install PyYAML==6.0.2 make lint-beta64-contract lint-artifacthub: From 35c323c9e2d4549ae0ae4808b0b4a80c39719c34 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sat, 5 Sep 2026 15:08:11 +0200 Subject: [PATCH 14/14] docs(authup): keep the console role table at three columns --- .agents/references/authup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/references/authup.md b/.agents/references/authup.md index cdba7f4..25cbc49 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -11,7 +11,7 @@ chart `appVersion`. | One `authup/authup` image with direct CLI args | `Dockerfile`, `entrypoint.sh` | `authup.appImage`; every application Deployment | | Combined service: `start` | `apps/authup/src/commands/start.ts` | `server/deployment.yaml` default | | API only: `start core` | `apps/authup/src/module.ts`, command tests | server when `server.splitConsoles=true` | -| Split consoles: `start console auth|admin|account` | `apps/authup/src/console/`, `apps/server-*-console/` | the three console directories | +| Split consoles: `start console auth`, `start console admin`, `start console account` | `apps/authup/src/console/`, `apps/server-*-console/` | the three console directories | | Background worker: `start worker` | `apps/authup/src/module.ts`, `apps/server-core/src/app/modules/components/module.ts` | `worker/deployment.yaml` | | Migration: `migration run` | `apps/server-core/src/cli/commands/migration.ts` | `server/migration-job.yaml` | | Core port 3000; console ports 3020/3021/3022 | `packages/server-config/src/sections/*/schema.ts`, `Dockerfile` | role `containerPorts` and Services |