diff --git a/.agents/architecture.md b/.agents/architecture.md index e6ad8f6..2647d7c 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -3,7 +3,7 @@ `DESIGN.md` is the authoritative rationale. This file is the compact list of operational invariants that template changes must preserve. -## Authup beta.64 runtime contract +## Authup runtime contract (through beta.68) 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` @@ -15,9 +15,10 @@ operational invariants that template changes must preserve. 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 + account consoles listen on 3020, 3021 and 3022. The worker health listener + uses `worker.containerPorts.http` (3000), wired to `WORKER_PORT` and an HTTP + readiness probe. It has no Service or liveness probe. +4. **Worker ownership is explicit.** `worker.enabled=true` (the default) 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. It requires the server diff --git a/.agents/references/authup.md b/.agents/references/authup.md index f6b947d..c119f10 100644 --- a/.agents/references/authup.md +++ b/.agents/references/authup.md @@ -1,9 +1,32 @@ # authup application mapping 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.67, the +at `/opt/projects/authup/authup`. This mapping is pinned to v1.0.0-beta.68, the chart `appVersion`. +Verified against tag commit `114f1bbc2dd16eb8be859e56e683b2fc9cd226b5`. +Beta.68 adds the worker health listener: + +- `apps/server-core/src/app/modules/components/health.ts` serves `GET /` and + `HEAD /` with 200/503; other paths return 404. `app/factory.ts` includes this + module only for the worker role, after database schema verification. +- `packages/server-config/src/sections/core/schema.ts` reads `WORKER_PORT` + (`core.worker.port`), falling back to `PORT` when unset. The listener binds + `core.host`. The chart sets `WORKER_PORT` from `worker.containerPorts.http` + so the listener and named readiness port agree even with shared config. +- `components/module.ts` reports a sweep overdue after five minutes without a + successful pass (including the startup allowance), or after a running pass + exceeds 30 minutes. `docs/src/guide/deployment/worker.md` recommends readiness + only: database outages should not trigger liveness restarts. +- The image and CLI healthchecks still follow `PORT`, not `WORKER_PORT`. + Kubernetes uses the chart's explicit readiness probe instead. +- CLI role args, database ownership and worker secret requirements are unchanged. + The chart enables the dedicated worker by default; `worker.enabled=false` + preserves in-process sweeps. Fresh-install workers can restart until the + server initializes the schema; the optional migration Job orders upgrades. + +## Previous deployment changes + beta.66 to beta.67 changed no image entrypoint, CLI role, port or environment-variable contract the chart owns: `Dockerfile`, `entrypoint.sh` and `packages/client-auth-console` are unchanged, and `apps/authup/src` only @@ -27,7 +50,7 @@ Deployment-facing facts, none of which needs a chart value - The new config field `eventLogAggregateRetentionDays` (`EVENT_LOG_AGGREGATE_RETENTION_DAYS`, default 0 = forever, `packages/server-config/src/sections/core/schema.ts`) is reachable through - the long tail (`server.config.core.eventLogAggregateRetentionDays`). The + the long tail (`server.config.EVENT_LOG_AGGREGATE_RETENTION_DAYS`). The rollup task runs wherever the worker sweeps run: the `start` process, or the `start worker` Deployment when `worker.enabled=true`. - Earlier deployment facts still hold: the device authorization grant rides @@ -46,10 +69,11 @@ Deployment-facing facts, none of which needs a chart value | 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 | +| Worker health port, default 3000 | `sections/core/schema.ts`, `components/health.ts` | `worker.containerPorts.http`, readiness probe; no Service | 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. +`client/admin-console`. The worker listener serves health only; do not route +API traffic to it or use its sweep-health status for liveness. ## Unified configuration @@ -63,6 +87,7 @@ The single schema is in `packages/server-config/src/`. Every service reads one | `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 | +| `WORKER_PORT` | `sections/core/schema.ts` | worker health listener and readiness probe | | `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 | @@ -72,10 +97,11 @@ 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 +- `start core` serves the API with optional in-process sweeps. 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. + does not need SMTP or bootstrap identity secrets, and runs no migrations. + Its health listener is used for readiness only. - 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 diff --git a/.agents/structure.md b/.agents/structure.md index 7cb674c..01939cb 100644 --- a/.agents/structure.md +++ b/.agents/structure.md @@ -22,11 +22,11 @@ authup/helm ├── README.md.gotmpl ├── BREAKING.md # migration ledger for the 0.x line ├── ci/ - │ ├── default-values.yaml # combined server + built-in PostgreSQL + │ ├── default-values.yaml # combined server + worker + built-in PostgreSQL │ ├── mysql-values.yaml │ ├── external-db-values.yaml │ ├── valkey-values.yaml # replicas + migration hook - │ ├── server-only-values.yaml # optional admin/account surfaces disabled + │ ├── server-only-values.yaml # worker and optional admin/account surfaces disabled │ ├── split-values.yaml # split consoles + worker + restrictive policies │ ├── theme-values.yaml │ └── manifests/postgres.yaml @@ -43,7 +43,7 @@ authup/helm ├── 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 + ├── worker/ # `start worker`; HTTP readiness, no Service ├── postgresql/ # vendored single-instance backing service ├── mysql/ └── valkey/ @@ -54,7 +54,8 @@ authup/helm 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`. +3020, 3021 and 3022. `worker.enabled=true` adds `start worker` by default; +setting it to false keeps background sweeps in the server. The similar role directories are deliberate. Authup roles have different ports, probes, secrets and Services, and authentik reverted a generic role loop diff --git a/.agents/testing.md b/.agents/testing.md index 43803da..17be6a3 100644 --- a/.agents/testing.md +++ b/.agents/testing.md @@ -17,6 +17,11 @@ 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. +The script retains its beta.64 filename and Make target, but tracks the current +appVersion. Beta.68 coverage includes the default worker, its opt-out, readiness +probe overrides and diagnostics, and the configured health port. The +server-only CI fixture disables the worker to keep the in-process path covered. + ## Useful renders ```bash diff --git a/AGENTS.md b/AGENTS.md index 75935e6..4e21bd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ changing chart architecture. ## Quick Reference ```bash -make test # lint + render matrix + coverage + beta.64 contract +make test # lint + render matrix + coverage + runtime 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) diff --git a/DESIGN.md b/DESIGN.md index d72ce75..b21d58b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -10,9 +10,9 @@ PrivateAIM/helm. 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 +2. Deploy every supported Authup v1.0.0-beta.68 role from the one upstream `authup/authup` image. -3. Keep a default install useful: a combined Authup server plus built-in +3. Keep a default install useful: a combined Authup server, dedicated worker and 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. @@ -35,21 +35,25 @@ Authup beta.64 replaces app-path arguments with direct CLI roles: | 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 | +| worker | `start worker` | 3000 (health) | yes | | 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 +`server.splitConsoles=false` keeps the API and enabled consoles in one +Deployment, alongside the dedicated worker. 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 +The worker is a separate Deployment when `worker.enabled=true` (the default). 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. +database, cache, configuration and log mechanics with core. Since beta.68 it +serves `GET /` health reports on `WORKER_PORT`, configured through +`worker.containerPorts.http` (3000 by default). A readiness probe checks sweep +health; no Service is needed for kubelet probes. There is no liveness probe: +database failures must not cause restart loops. It does not receive SMTP or +bootstrap identity secrets because those modules are outside the worker role. +Setting `worker.enabled=false` keeps the sweeps in the server process. Each role has explicit templates. Their structural duplication is deliberate: authentik built and later removed a generic role loop because heterogeneous @@ -257,7 +261,7 @@ idempotent on reruns. | 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 | +| Worker Service or liveness probe | kubelet probes need no Service; failed sweeps should affect readiness without restarts | | 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 | diff --git a/Makefile b/Makefile index 0ca927c..054b6ac 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ lint: lint-values-coverage: python3 scripts/check-values-coverage.py charts/authup -## Assert the authup beta.64 process, configuration and filesystem contract. +## Assert the authup process, configuration and filesystem contract through beta.68. lint-beta64-contract: python3 scripts/check-beta64-contract.py charts/authup all diff --git a/README.md b/README.md index 095f47a..a6ba390 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,9 @@ valkey: enabled: true ``` -Set `server.splitConsoles=true` to run the consoles as separate workloads and -`worker.enabled=true` for a dedicated background worker. +Set `server.splitConsoles=true` to run the consoles as separate workloads. +A dedicated background worker is enabled by default with an HTTP readiness +probe. Set `worker.enabled=false` to run background sweeps in the server. See the [chart README](./charts/authup/README.md) for every parameter and the operational notes (GitOps caveats, scaling rules, the write-once encryption @@ -98,7 +99,7 @@ key). | Chart | Description | |---|---| -| [authup](./charts/authup) | Authup API and consoles (combined or split), optional worker, optional built-in PostgreSQL / MySQL / Valkey | +| [authup](./charts/authup) | Authup API and consoles (combined or split), dedicated worker, optional built-in PostgreSQL / MySQL / Valkey | ## Documentation diff --git a/charts/authup/BREAKING.md b/charts/authup/BREAKING.md index 26625e6..a3e77d8 100644 --- a/charts/authup/BREAKING.md +++ b/charts/authup/BREAKING.md @@ -3,6 +3,16 @@ This chart uses `0.major.minor` versioning while below 1.0.0: breaking changes land on the middle digit. Every entry lists the value migrations required. +## Unreleased + +- `worker.enabled` now defaults to `true`, adding a worker Deployment and moving + background sweeps out of the server. Set it to `false` to keep the previous + topology. Deployments with `server.enabled=false` must also disable the worker. +- The default image is Authup v1.0.0-beta.68. If pinning an older image, disable + `worker.readinessProbe.enabled` because older workers have no health listener. +- `WORKER_PORT` is now owned by `worker.containerPorts.http`; move any + `server.config.WORKER_PORT` override there. + ## 0.4.0 - The chart now requires the Authup v1.0.0-beta.64 CLI. Default server args are diff --git a/charts/authup/Chart.yaml b/charts/authup/Chart.yaml index e7c148e..d399491 100644 --- a/charts/authup/Chart.yaml +++ b/charts/authup/Chart.yaml @@ -5,7 +5,7 @@ description: Authup is an authentication & authorization system. This chart built-in PostgreSQL, MySQL and Valkey instances. type: application version: 0.4.3 -appVersion: "1.0.0-beta.67" +appVersion: "1.0.0-beta.68" kubeVersion: ">=1.25.0-0" home: https://authup.org icon: https://raw.githubusercontent.com/authup/helm/master/assets/icon.svg @@ -32,8 +32,6 @@ annotations: url: https://github.com/authup/helm artifacthub.io/changes: | - kind: changed - description: Track Authup v1.0.0-beta.67; no image entrypoint, CLI or environment-variable contract changes + description: Track Authup v1.0.0-beta.68 and enable the dedicated worker by default - kind: changed - description: Authup v1.0.0-beta.67 ships migration 1789930726252-PathsAndEventAggregates, applied by the migration Job or the server boot - - kind: changed - description: Authup now pins database sessions to UTC; a database that ran in local time needs its historical timestamps converted once (see the Authup upgrading guide) + description: Probe worker sweep health with a configurable HTTP readiness check diff --git a/charts/authup/README.md b/charts/authup/README.md index eccecfb..0c1b4e3 100644 --- a/charts/authup/README.md +++ b/charts/authup/README.md @@ -7,15 +7,15 @@ # authup ![Version](https://img.shields.io/badge/Version-0.4.3?style=flat-square&color=informational) -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.67](https://img.shields.io/badge/AppVersion-1.0.0--beta.67-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-beta.68](https://img.shields.io/badge/AppVersion-1.0.0--beta.68-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.67's +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.68'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`) +- a dedicated background worker (`start worker`), enabled by default - 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. @@ -33,7 +33,14 @@ helm install authup authup/authup helm install authup oci://ghcr.io/authup/helm/authup ``` -The default install starts the combined server and a built-in PostgreSQL. +The default install starts the combined server, a dedicated worker and built-in PostgreSQL. +Set `worker.enabled=false` to keep background sweeps in the server process. +The worker's readiness probe checks sweep health at `GET /` on port 3000; +`worker.containerPorts.http` changes its listener and probe port together. +There is no worker Service or liveness probe: a database outage marks the +worker unready without restarting it. On a fresh install the worker may restart +until the server initializes the schema; enable `server.migration.enabled` to +run migrations before upgrade rollouts. Retrieve the generated admin password: ```bash @@ -52,9 +59,6 @@ server: migration: enabled: true -worker: - enabled: true - postgresql: enabled: false externalDatabase: @@ -718,9 +722,11 @@ Kubernetes: `>=1.25.0-0` | 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.containerPorts.http | int | `3000` | Worker health listener port (WORKER_PORT) | | worker.containerSecurityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":false,"runAsNonRoot":false,"runAsUser":0,"seccompProfile":{"type":"RuntimeDefault"}}` | Container security context | +| worker.customReadinessProbe | object | `{}` | Custom readiness probe with exactly one handler (overrides readinessProbe) | | 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). 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.enabled | bool | `true` | 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) | @@ -741,6 +747,12 @@ Kubernetes: `>=1.25.0-0` | worker.podLabels | object | `{}` | Pod labels (tpl-rendered) | | worker.podSecurityContext | object | `{"enabled":true,"fsGroup":1000}` | Pod security context | | worker.priorityClassName | string | `""` | Priority class name | +| worker.readinessProbe.enabled | bool | `true` | Check sweep health without restarting the worker on database outages | +| worker.readinessProbe.failureThreshold | int | `3` | | +| worker.readinessProbe.initialDelaySeconds | int | `0` | | +| worker.readinessProbe.periodSeconds | int | `30` | | +| worker.readinessProbe.successThreshold | int | `1` | | +| worker.readinessProbe.timeoutSeconds | int | `5` | | | 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 | diff --git a/charts/authup/README.md.gotmpl b/charts/authup/README.md.gotmpl index 6ee02b9..860a96c 100644 --- a/charts/authup/README.md.gotmpl +++ b/charts/authup/README.md.gotmpl @@ -28,7 +28,7 @@ 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`) +- a dedicated background worker (`start worker`), enabled by default - 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. @@ -46,7 +46,14 @@ helm install authup authup/authup helm install authup oci://ghcr.io/authup/helm/authup ``` -The default install starts the combined server and a built-in PostgreSQL. +The default install starts the combined server, a dedicated worker and built-in PostgreSQL. +Set `worker.enabled=false` to keep background sweeps in the server process. +The worker's readiness probe checks sweep health at `GET /` on port 3000; +`worker.containerPorts.http` changes its listener and probe port together. +There is no worker Service or liveness probe: a database outage marks the +worker unready without restarting it. On a fresh install the worker may restart +until the server initializes the schema; enable `server.migration.enabled` to +run migrations before upgrade rollouts. Retrieve the generated admin password: ```bash @@ -65,9 +72,6 @@ server: migration: enabled: true -worker: - enabled: true - postgresql: enabled: false externalDatabase: diff --git a/charts/authup/ci/default-values.yaml b/charts/authup/ci/default-values.yaml index 6862e0d..78930d4 100644 --- a/charts/authup/ci/default-values.yaml +++ b/charts/authup/ci/default-values.yaml @@ -1,4 +1,4 @@ -# Baseline: combined server with built-in PostgreSQL. +# Baseline: combined server, dedicated worker and 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. diff --git a/charts/authup/ci/server-only-values.yaml b/charts/authup/ci/server-only-values.yaml index 9e5b3d6..ea25681 100644 --- a/charts/authup/ci/server-only-values.yaml +++ b/charts/authup/ci/server-only-values.yaml @@ -1,5 +1,7 @@ # Combined server with the optional admin and account surfaces disabled. The # auth pages remain available because they own the login flow. +worker: + enabled: false postgresql: persistence: enabled: false diff --git a/charts/authup/templates/_server-env.tpl b/charts/authup/templates/_server-env.tpl index dc0f4d5..8cd6628 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" "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" }} +{{- $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" "WORKER_PORT" "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) }} diff --git a/charts/authup/templates/worker/deployment.yaml b/charts/authup/templates/worker/deployment.yaml index 44c34a3..5c8542f 100644 --- a/charts/authup/templates/worker/deployment.yaml +++ b/charts/authup/templates/worker/deployment.yaml @@ -108,9 +108,26 @@ spec: env: {{- include "authup.server.secretEnv" (dict "context" $ "role" "worker") | nindent 12 }} - name: WORKER_ENABLED value: "true" + - name: WORKER_PORT + value: {{ .Values.worker.containerPorts.http | quote }} {{- if .Values.worker.extraEnvVars }} {{- include "authup.tplvalues.render" (dict "value" .Values.worker.extraEnvVars "context" $) | nindent 12 }} {{- end }} + ports: + - name: http + containerPort: {{ .Values.worker.containerPorts.http }} + protocol: TCP + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.worker.customReadinessProbe }} + readinessProbe: {{- include "authup.tplvalues.render" (dict "value" .Values.worker.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.worker.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: / + port: http + {{- omit .Values.worker.readinessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- end }} {{- if .Values.worker.resources }} resources: {{- toYaml .Values.worker.resources | nindent 12 }} {{- end }} diff --git a/charts/authup/values.schema.json b/charts/authup/values.schema.json index c44c84c..4290449 100644 --- a/charts/authup/values.schema.json +++ b/charts/authup/values.schema.json @@ -5974,6 +5974,25 @@ "title": "command", "type": "array" }, + "containerPorts": { + "additionalProperties": false, + "properties": { + "http": { + "default": 3000, + "description": "Worker health listener port (WORKER_PORT)", + "maximum": 65535, + "minimum": 1, + "required": [], + "title": "http", + "type": "integer" + } + }, + "required": [ + "http" + ], + "title": "containerPorts", + "type": "object" + }, "containerSecurityContext": { "additionalProperties": true, "description": "Container security context", @@ -6060,6 +6079,57 @@ ], "title": "containerSecurityContext" }, + "customReadinessProbe": { + "additionalProperties": true, + "description": "Custom readiness probe with exactly one handler (overrides readinessProbe)", + "oneOf": [ + { + "additionalProperties": false, + "required": [] + }, + { + "required": [ + "httpGet" + ] + }, + { + "required": [ + "tcpSocket" + ] + }, + { + "required": [ + "exec" + ] + }, + { + "required": [ + "grpc" + ] + } + ], + "properties": { + "exec": { + "required": [], + "type": "object" + }, + "grpc": { + "required": [], + "type": "object" + }, + "httpGet": { + "required": [], + "type": "object" + }, + "tcpSocket": { + "required": [], + "type": "object" + } + }, + "required": [], + "title": "customReadinessProbe", + "type": "object" + }, "disableRestartOnChanges": { "default": false, "description": "Disable checksum annotations that roll pods on configuration changes", @@ -6068,7 +6138,7 @@ "type": "boolean" }, "enabled": { - "default": false, + "default": true, "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", @@ -6262,6 +6332,58 @@ "title": "priorityClassName", "type": "string" }, + "readinessProbe": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "description": "Check sweep health without restarting the worker on database outages", + "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": "readinessProbe", + "type": "object" + }, "replicaCount": { "default": 1, "description": "Number of worker replicas (one is normally sufficient)", @@ -6382,6 +6504,8 @@ "required": [ "enabled", "replicaCount", + "containerPorts", + "readinessProbe", "command", "args", "extraEnvVars", diff --git a/charts/authup/values.yaml b/charts/authup/values.yaml index f4ea9ee..e4c9b89 100644 --- a/charts/authup/values.yaml +++ b/charts/authup/values.yaml @@ -1667,9 +1667,43 @@ worker: # -- 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 + enabled: true # -- Number of worker replicas (one is normally sufficient) replicaCount: 1 + containerPorts: + # @schema + # type: integer + # required: true + # minimum: 1 + # maximum: 65535 + # @schema + # -- Worker health listener port (WORKER_PORT) + http: 3000 + readinessProbe: + # -- Check sweep health without restarting the worker on database outages + enabled: true + initialDelaySeconds: 0 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + # @schema + # additionalProperties: true + # type: object + # properties: + # httpGet: {type: object} + # tcpSocket: {type: object} + # exec: {type: object} + # grpc: {type: object} + # oneOf: + # - additionalProperties: false + # - required: [httpGet] + # - required: [tcpSocket] + # - required: [exec] + # - required: [grpc] + # @schema + # -- Custom readiness probe with exactly one handler (overrides readinessProbe) + customReadinessProbe: {} # -- Override the container command command: [] # -- Override the container args diff --git a/scripts/check-beta64-contract.py b/scripts/check-beta64-contract.py index 724b94e..b704cbf 100644 --- a/scripts/check-beta64-contract.py +++ b/scripts/check-beta64-contract.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Assert the Authup beta.64 runtime contract against rendered manifests.""" +"""Assert the Authup runtime contract (through beta.68) against rendered manifests.""" import subprocess import sys @@ -153,10 +153,55 @@ def check_base(): 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 + assert env_value(container(server), "WORKER_ENABLED") == "false" assert env_value(container(server), "MIGRATION_ENABLED") is None assert server["metadata"]["labels"]["app.kubernetes.io/version"] == app_version + worker = container(one(documents, "Deployment", "worker")) + assert worker["args"] == ["start", "worker"] + assert env_value(worker, "WORKER_ENABLED") == "true" + assert env_value(worker, "WORKER_PORT") == "3000" + assert worker["ports"] == [{"name": "http", "containerPort": 3000, "protocol": "TCP"}] + assert worker["readinessProbe"]["httpGet"] == {"path": "/", "port": "http"} + assert worker["readinessProbe"]["periodSeconds"] == 30 + assert not ({"startupProbe", "livenessProbe"} & set(worker)) + assert not any( + doc["kind"] == "Service" + and doc["metadata"]["labels"].get("app.kubernetes.io/component") == "worker" + for doc in documents + ) + + disabled = render({"worker": {"enabled": False}}) + assert "worker" not in component_deployments(disabled) + assert env_value(container(one(disabled, "Deployment", "server")), "WORKER_ENABLED") is None + + for values in ( + {"worker": {"readinessProbe": {"enabled": False}}}, + {"diagnosticMode": {"enabled": True}}, + {"diagnosticMode": {"enabled": True}, "worker": { + "customReadinessProbe": {"exec": {"command": ["true"]}}, + }}, + ): + worker = container(one(render(values), "Deployment", "worker")) + assert "readinessProbe" not in worker + + custom = render({"worker": { + "containerPorts": {"http": 3005}, + "readinessProbe": {"periodSeconds": 15}, + }}) + worker = container(one(custom, "Deployment", "worker")) + assert env_value(worker, "WORKER_PORT") == "3005" + assert worker["ports"][0]["containerPort"] == 3005 + assert worker["readinessProbe"]["httpGet"]["port"] == "http" + assert worker["readinessProbe"]["periodSeconds"] == 15 + + custom = render({"worker": { + "readinessProbe": {"enabled": False}, + "customReadinessProbe": {"exec": {"command": ["{{ .Release.Name }}"]}}, + }}) + worker = container(one(custom, "Deployment", "worker")) + assert worker["readinessProbe"] == {"exec": {"command": ["test"]}} + configured_values = { "server": { "configuration": "core:\n trustProxy: '1'\n", @@ -258,9 +303,10 @@ def check_split(): worker = one(documents, "Deployment", "worker") worker_container = container(worker) assert worker_container["args"] == ["start", "worker"] - assert "ports" not in worker_container + assert worker_container["ports"] == [{"name": "http", "containerPort": 3000, "protocol": "TCP"}] + assert worker_container["readinessProbe"]["httpGet"] == {"path": "/", "port": "http"} assert not ( - {"startupProbe", "livenessProbe", "readinessProbe"} + {"startupProbe", "livenessProbe"} & set(worker_container) ) worker_env = effective_env(worker, documents) @@ -719,7 +765,24 @@ def check_validations(): {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"): + for probe in ( + {"periodSeconds": 15}, + {"httpGet": {"path": "/", "port": "http"}, "exec": {"command": ["true"]}}, + {"httpGet": None, "periodSeconds": 15}, + ): + render_fails({"worker": {"customReadinessProbe": probe}}, "customReadinessProbe") + for handler, settings in ( + ("httpGet", {"path": "/", "port": "http"}), + ("tcpSocket", {"port": "http"}), + ("exec", {"command": ["true"]}), + ("grpc", {"port": 3000}), + ): + probe = {handler: settings, "periodSeconds": 15} + worker = one(render({"worker": {"customReadinessProbe": probe}}), "Deployment", "worker") + assert container(worker)["readinessProbe"] == probe + for port in (0, 65536, "invalid", None): + render_fails({"worker": {"containerPorts": {"http": port}}}, "http") + for name in ("ADMIN_CONSOLE_ENABLED", "WORKER_ENABLED", "WORKER_PORT", "MIGRATION_ENABLED"): render_fails( {"server": {"config": {name: "false"}}}, f"server.config.{name} collides with a first-class chart value", @@ -759,4 +822,4 @@ def check_validations(): raise SystemExit(f"unknown contract case {case!r}: choose {', '.join(checks)}") checks[case]() -print(f"beta.64 {case} contract OK") +print(f"Authup {app_version} {case} contract OK")