From 5ed51fe35c72c6293cda709d12b35954c05c665c Mon Sep 17 00:00:00 2001 From: Abdullah Kaya Date: Thu, 22 Oct 2026 17:48:14 +0300 Subject: [PATCH 1/5] feat(kubernetes): add the libredb-platform Helm chart Install LibreDB Platform on Kubernetes with Helm. The chart is the installer's Kubernetes mode driven by Helm: a bootstrap hook and jobs run the installer's own code, bundled into the chart, so a Helm install generates the same secrets, CA, PostgreSQL roles, Keycloak realm and app environments as a Docker install, with Studio as a pinned dependency. The chart covers managed or external PostgreSQL and Keycloak, generated or site TLS, upgrades, rollbacks and refused downgrades, a restart job for changed inputs, Argo CD, OpenShift's restricted-v2 SCC, seal key and secret rotation through values, amd64 scheduling for the Platform image, backup and restore, and an install without network access. Tests: unit tests in installer/test/unit/kubernetes hold the rendered chart to the installer, and the scripts in charts/libredb-platform/tests/e2e drive kind, Argo CD and OpenShift with a browser suite in installer/e2e/kubernetes. Docs: docs/deploy/install-kubernetes.md and the chart README. --- .github/workflows/installer.yml | 14 + .gitignore | 3 + README.md | 2 +- ROADMAP.md | 2 +- charts/libredb-platform/.helmignore | 7 + charts/libredb-platform/Chart.lock | 6 + charts/libredb-platform/Chart.yaml | 31 + charts/libredb-platform/README.md | 85 +++ .../files/libredb-jobs.cjs.gz | Bin 0 -> 173137 bytes charts/libredb-platform/templates/NOTES.txt | 47 ++ .../libredb-platform/templates/_helpers.tpl | 367 +++++++++++ .../templates/bootstrap-hook.yaml | 269 ++++++++ charts/libredb-platform/templates/jobs.yaml | 377 +++++++++++ .../libredb-platform/templates/keycloak.yaml | 220 +++++++ .../templates/networkpolicy.yaml | 52 ++ .../libredb-platform/templates/platform.yaml | 172 +++++ .../libredb-platform/templates/postgres.yaml | 223 +++++++ .../tests/e2e/coredns-rewrite.sh | 13 + .../tests/e2e/kind-config.yaml | 17 + .../libredb-platform/tests/e2e/run-argocd.sh | 153 +++++ charts/libredb-platform/tests/e2e/run-e2e.sh | 154 +++++ .../tests/e2e/run-external.sh | 224 +++++++ .../tests/e2e/run-lifecycle.sh | 125 ++++ .../libredb-platform/tests/e2e/run-offline.sh | 114 ++++ .../tests/e2e/run-openshift.sh | 155 +++++ .../tests/e2e/run-resilience.sh | 109 ++++ .../libredb-platform/tests/e2e/run-restore.sh | 115 ++++ .../tests/e2e/run-rotation.sh | 43 ++ charts/libredb-platform/tests/e2e/run-soak.sh | 155 +++++ .../libredb-platform/tests/e2e/run-upgrade.sh | 97 +++ .../tests/e2e/traefik-values.yaml | 23 + .../tests/e2e/values-kind.yaml | 20 + .../tests/e2e/values-openshift.yaml | 21 + charts/libredb-platform/values.yaml | 276 ++++++++ docs/deploy/install-docker.md | 2 +- docs/deploy/install-kubernetes.md | 343 ++++++++++ docs/deploy/platform-runtime.md | 2 +- installer/README.md | 2 +- installer/e2e/helpers/studio.ts | 4 +- installer/e2e/kubernetes/helm-install.k8s.ts | 211 ++++++ installer/e2e/kubernetes/k8s.ts | 232 +++++++ installer/e2e/kubernetes/playwright.config.ts | 30 + installer/e2e/kubernetes/restore.k8s.ts | 116 ++++ installer/e2e/kubernetes/rotation.k8s.ts | 303 +++++++++ installer/package.json | 3 +- installer/scripts/build-helm-jobs.ts | 48 ++ installer/src/adapters/kubernetes-api.ts | 222 +++++++ installer/src/cli/kubernetes-job.ts | 114 ++++ installer/src/kubernetes/bootstrap.ts | 612 ++++++++++++++++++ installer/src/kubernetes/keycloak.ts | 213 ++++++ installer/src/kubernetes/names.ts | 91 +++ .../test/unit/adapters/kubernetes-api.test.ts | 166 +++++ .../test/unit/kubernetes/bootstrap.test.ts | 478 ++++++++++++++ installer/test/unit/kubernetes/bundle.test.ts | 8 + installer/test/unit/kubernetes/chart.test.ts | 451 +++++++++++++ installer/test/unit/kubernetes/fixture.ts | 127 ++++ .../test/unit/kubernetes/job-exit.test.ts | 24 + .../test/unit/kubernetes/keycloak.test.ts | 199 ++++++ 58 files changed, 7684 insertions(+), 8 deletions(-) create mode 100644 charts/libredb-platform/.helmignore create mode 100644 charts/libredb-platform/Chart.lock create mode 100644 charts/libredb-platform/Chart.yaml create mode 100644 charts/libredb-platform/README.md create mode 100644 charts/libredb-platform/files/libredb-jobs.cjs.gz create mode 100644 charts/libredb-platform/templates/NOTES.txt create mode 100644 charts/libredb-platform/templates/_helpers.tpl create mode 100644 charts/libredb-platform/templates/bootstrap-hook.yaml create mode 100644 charts/libredb-platform/templates/jobs.yaml create mode 100644 charts/libredb-platform/templates/keycloak.yaml create mode 100644 charts/libredb-platform/templates/networkpolicy.yaml create mode 100644 charts/libredb-platform/templates/platform.yaml create mode 100644 charts/libredb-platform/templates/postgres.yaml create mode 100755 charts/libredb-platform/tests/e2e/coredns-rewrite.sh create mode 100644 charts/libredb-platform/tests/e2e/kind-config.yaml create mode 100755 charts/libredb-platform/tests/e2e/run-argocd.sh create mode 100755 charts/libredb-platform/tests/e2e/run-e2e.sh create mode 100755 charts/libredb-platform/tests/e2e/run-external.sh create mode 100755 charts/libredb-platform/tests/e2e/run-lifecycle.sh create mode 100755 charts/libredb-platform/tests/e2e/run-offline.sh create mode 100755 charts/libredb-platform/tests/e2e/run-openshift.sh create mode 100755 charts/libredb-platform/tests/e2e/run-resilience.sh create mode 100755 charts/libredb-platform/tests/e2e/run-restore.sh create mode 100755 charts/libredb-platform/tests/e2e/run-rotation.sh create mode 100755 charts/libredb-platform/tests/e2e/run-soak.sh create mode 100755 charts/libredb-platform/tests/e2e/run-upgrade.sh create mode 100644 charts/libredb-platform/tests/e2e/traefik-values.yaml create mode 100644 charts/libredb-platform/tests/e2e/values-kind.yaml create mode 100644 charts/libredb-platform/tests/e2e/values-openshift.yaml create mode 100644 charts/libredb-platform/values.yaml create mode 100644 docs/deploy/install-kubernetes.md create mode 100644 installer/e2e/kubernetes/helm-install.k8s.ts create mode 100644 installer/e2e/kubernetes/k8s.ts create mode 100644 installer/e2e/kubernetes/playwright.config.ts create mode 100644 installer/e2e/kubernetes/restore.k8s.ts create mode 100644 installer/e2e/kubernetes/rotation.k8s.ts create mode 100644 installer/scripts/build-helm-jobs.ts create mode 100644 installer/src/adapters/kubernetes-api.ts create mode 100644 installer/src/cli/kubernetes-job.ts create mode 100644 installer/src/kubernetes/bootstrap.ts create mode 100644 installer/src/kubernetes/keycloak.ts create mode 100644 installer/src/kubernetes/names.ts create mode 100644 installer/test/unit/adapters/kubernetes-api.test.ts create mode 100644 installer/test/unit/kubernetes/bootstrap.test.ts create mode 100644 installer/test/unit/kubernetes/bundle.test.ts create mode 100644 installer/test/unit/kubernetes/chart.test.ts create mode 100644 installer/test/unit/kubernetes/fixture.ts create mode 100644 installer/test/unit/kubernetes/job-exit.test.ts create mode 100644 installer/test/unit/kubernetes/keycloak.test.ts diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 5e632e1..86662bf 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -5,11 +5,13 @@ on: branches: [main] paths: - 'installer/**' + - 'charts/**' - 'tests/fixtures/governance/STUDIO_TAG' - '.github/workflows/installer.yml' pull_request: paths: - 'installer/**' + - 'charts/**' - 'tests/fixtures/governance/STUDIO_TAG' - '.github/workflows/installer.yml' @@ -40,6 +42,18 @@ jobs: run: bun run embed env: LIBREDB_STUDIO_REPO: ${{ github.workspace }}/studio-src + # The Helm chart's tests (test/unit/kubernetes/chart.test.ts) render the chart with a pinned Helm; its Studio + # dependency is the chart that embed just downloaded and checked against the release manifest's sha256. + - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 + with: + version: v3.21.3 + - name: Lint the Helm chart + working-directory: . + run: | + mkdir -p charts/libredb-platform/charts + cp installer/generated/libredb-studio-*.tgz charts/libredb-platform/charts/ + helm lint charts/libredb-platform --strict -f charts/libredb-platform/tests/e2e/values-kind.yaml \ + --set platform.license.value=ldl1.0123456789abcdef.eyJpZCI6IkxJTlQifQ.c2lnbmF0dXJl - run: bun run typecheck - run: bun run coverage:check - run: bun run build diff --git a/.gitignore b/.gitignore index 591d2a8..00a3c07 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ next-env.d.ts /installer/keycloak-export/ /installer/ca.crt *.license + +# Helm chart dependencies, built with helm dependency build from Chart.lock +charts/*/charts/*.tgz diff --git a/README.md b/README.md index 39b77f9..c92e2c4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Write to [info@libredb.org](mailto:info@libredb.org) or join the waiting list at Platform 1.0.1 is the current release, published as the image `ghcr.io/libredb/libredb-platform`, and pairs with LibreDB Studio 1.0.0. It installs with Docker on one x86_64 Linux host: the installer deploys PostgreSQL, Keycloak, Platform, Studio and a Caddy edge, and can use an existing PostgreSQL server or Keycloak instead. -Kubernetes support follows in the next release. +On Kubernetes, the Helm chart [`charts/libredb-platform`](charts/libredb-platform) installs the same, as the [Kubernetes install guide](docs/deploy/install-kubernetes.md) describes. Cloud mode serves development and the hosted service of a later release, and it is not a supported way to install LibreDB. Download the installer from this repository's [GitHub releases](https://github.com/libredb/libredb-platform/releases) and follow the [Docker install guide](docs/deploy/install-docker.md). Every install needs a license key from LibreDB, and Platform does not start in self-hosted mode without one. diff --git a/ROADMAP.md b/ROADMAP.md index 12f4736..7f9b515 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,7 @@ - [ ] Studio users can connect to the platform with limited permissions(studio, account, support/tickets) - [ ] All libredb-studio database connections support -- [ ] Kubernetes +- [x] Kubernetes (Helm chart `charts/libredb-platform`) - [ ] GitOps --- diff --git a/charts/libredb-platform/.helmignore b/charts/libredb-platform/.helmignore new file mode 100644 index 0000000..2a43beb --- /dev/null +++ b/charts/libredb-platform/.helmignore @@ -0,0 +1,7 @@ +.DS_Store +.git/ +*.swp +*.bak +*.tmp +tests/ +README.md.gotmpl diff --git a/charts/libredb-platform/Chart.lock b/charts/libredb-platform/Chart.lock new file mode 100644 index 0000000..abaf0d1 --- /dev/null +++ b/charts/libredb-platform/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: libredb-studio + repository: https://libredb.org/libredb-studio/ + version: 0.1.100 +digest: sha256:a56f4096de2bd4d02e73a5b984ba8040490565a767be05a123ffb2abb88a92d6 +generated: "2026-10-20T02:08:21.66270654+03:00" diff --git a/charts/libredb-platform/Chart.yaml b/charts/libredb-platform/Chart.yaml new file mode 100644 index 0000000..f7257d1 --- /dev/null +++ b/charts/libredb-platform/Chart.yaml @@ -0,0 +1,31 @@ +apiVersion: v2 +name: libredb-platform +description: >- + LibreDB Platform, database access governance, with LibreDB Studio, on Kubernetes: Platform, its migrate job, + Studio under governance, and optionally a managed PostgreSQL and Keycloak. Source-available under the + Elastic License 2.0; every install needs a license from LibreDB. +type: application +# The chart version; appVersion is the Platform release whose images and Studio chart it pins +# (installer/release/release.config.json, which installer/test/unit/kubernetes/chart.test.ts compares). +version: 0.1.0 +appVersion: "1.0.1" +kubeVersion: ">=1.28.0-0" +home: https://libredb.org/platform/ +sources: + - https://github.com/libredb/libredb-platform +license: Elastic-2.0 +keywords: + - database + - governance + - audit + - keycloak + - libredb +maintainers: + - name: LibreDB + url: https://libredb.org +dependencies: + - name: libredb-studio + alias: studio + version: 0.1.100 + repository: https://libredb.org/libredb-studio/ + condition: studio.enabled diff --git a/charts/libredb-platform/README.md b/charts/libredb-platform/README.md new file mode 100644 index 0000000..f10b291 --- /dev/null +++ b/charts/libredb-platform/README.md @@ -0,0 +1,85 @@ +# libredb-platform Helm chart + +LibreDB Platform, database access governance, with LibreDB Studio, on Kubernetes. +The guide is [docs/deploy/install-kubernetes.md](../../docs/deploy/install-kubernetes.md); this file lists the values. +LibreDB Platform is source-available under the Elastic License 2.0, and every install needs a license from LibreDB. + +The chart is the installer's Kubernetes mode driven by Helm. +It renders the values as an installation configuration of `mode: kubernetes`, which its jobs validate with the installer's schema and turn into Secrets, certificates, environments, a PostgreSQL provisioning script and a Keycloak realm with the installer's own code, bundled in `files/libredb-jobs.cjs.gz` (`bun run build:helm-jobs` in `installer/` rebuilds it; a unit test fails while it is stale). + +## Install + +```sh +helm dependency build charts/libredb-platform +helm install libredb charts/libredb-platform -n libredb --create-namespace \ + -f libredb-values.yaml --set-file platform.license.value=acme.license \ + --wait --wait-for-jobs --timeout 10m +``` + +## What it deploys + +| Object | When | +| --- | --- | +| Job `libredb-bootstrap` (pre-install, pre-upgrade and pre-rollback hook) with its ServiceAccount and Role | always | +| StatefulSet and Service `libredb-postgres`, provisioning Job | `postgresql.mode: managed` | +| Deployment and Services `keycloak` and `keycloak-edge`, Ingress `keycloak` | `keycloak.mode: managed` | +| Job `libredb-keycloak-configure-` | a managed Keycloak, or `keycloak.external.provisioning: installer` | +| Job `libredb-platform-migrate-` | always | +| Job `libredb-restart` (post-install, post-upgrade and post-rollback hook) with its ServiceAccount and Role | always | +| Deployment, Service, Ingress and PVC `libredb-platform` | always | +| The Studio chart (pinned by the release manifest) under governance | `studio.enabled` | +| NetworkPolicies `libredb-postgres` and `keycloak` | `networkPolicy.enabled` | + +The bootstrap job writes `libredb-secrets` (generated secrets and seal keys, never replaced), `libredb-tls` (internal CA and leaves), `libredb-platform-env`, `libredb-platform-migrate-env`, `libredb-studio-env`, `libredb-keycloak-config`, `libredb-ca` and the objects of the managed PostgreSQL and Keycloak. +They carry no Helm ownership and outlive `helm uninstall`; back up `libredb-secrets` and `libredb-tls`. +When one of them changes, the restart job restarts the workloads that read it, once Helm applied the release. + +## Values + +| Value | Default | Meaning | +| --- | --- | --- | +| `installation.name` | `libredb` | The installation's name, which names the internal CA | +| `hosts.platform`, `hosts.studio`, `hosts.auth` | | Required: the public hosts, resolvable from browsers and from the cluster's pods | +| `platform.organizationName` | | Required: the name of the installation's one tenant | +| `platform.queryLogRetentionDays` | `null` (90) | Days Platform keeps query events | +| `platform.license.value` | | The license token; pass it with `--set-file` | +| `platform.license.existingSecret`, `platform.license.key` | `""`, `license` | Or a Secret that holds the token | +| `platform.image` | the release's, by digest | Platform's image | +| `platform.resources`, `nodeSelector`, `tolerations`, `affinity`, `podAnnotations`, `podLabels` | | Platform's pod; `nodeSelector` and `tolerations` also place the jobs that run the Platform image, and `nodeSelector` holds `kubernetes.io/arch: amd64` unless it names another arch | +| `platform.persistence.enabled`, `size`, `storageClass`, `accessModes` | `true`, `5Gi` | Support attachments under `/app/uploads` | +| `tls.mode` | `generated` | `generated`: the internal CA issues the ingress certificates; `secrets`: your TLS Secrets | +| `tls.secrets.platform`, `studio`, `auth` | | With `tls.mode: secrets`, the TLS Secret of each host (`auth` only with a managed Keycloak); repeat `studio` as `studio.ingress.tls[0].secretName` | +| `tls.caBundle` | | With `tls.mode: secrets`, the PEM of the CA that issued them (required) | +| `ingress.enabled`, `className`, `annotations` | `true` | The Ingress of Platform and of a managed Keycloak | +| `postgresql.mode` | `managed` | `managed` or `external` | +| `postgresql.managed.image`, `persistence.size`, `persistence.storageClass`, `resources`, `nodeSelector`, `tolerations`, `affinity` | the release's image, `10Gi` | The managed server | +| `postgresql.external.host`, `port`, `caBundle` | `""`, `5432` | The external server, provisioned once by a DBA with Secret `libredb-postgres-dba-script` | +| `keycloak.mode` | `managed` | `managed` or `external` | +| `keycloak.realm` | `libredb` | The realm | +| `keycloak.firstAdmin.enabled`, `username` | `null`, `libredb-admin` | The first admin account; enabled by default with a managed Keycloak | +| `keycloak.userManager.enabled`, `username` | `null`, `libredb-user-manager` | The user manager account; enabled by default with a managed Keycloak | +| `keycloak.managed.image`, `resources`, `nodeSelector`, `tolerations`, `affinity` | the release's image | The managed Keycloak | +| `keycloak.managed.edge.image`, `resources` | the release's Caddy | The installer's edge in front of Keycloak's public paths | +| `keycloak.external.baseUrl`, `adminUrl` | | Your Keycloak; `hosts.auth` must be the host of `baseUrl` | +| `keycloak.external.provisioning` | `installer` | `installer`: the chart's job configures the realm; `manual`: your identity team does | +| `keycloak.external.adminRealm`, `adminSecret`, `adminClientId`, `adminUser` | `master` | With `installer`: the Secret with `admin-client-secret` (client) or `admin-password` (user) | +| `keycloak.external.clientSecretsSecret` | | With `manual`: the Secret with `studio-client-secret` and `platform-client-secret` | +| `keycloak.external.caBundle` | | The PEM of your Keycloak's CA, when a private CA issued its certificate | +| `network.httpsProxy` | | An http(s) proxy for the apps' outbound HTTPS, without credentials | +| `jobs.backoffLimit`, `activeDeadlineSeconds`, `resources` | `6`, `900` | The chart's jobs | +| `networkPolicy.enabled` | `true` | PostgreSQL and Keycloak's Admin API reachable only from this release's pods | +| `platform.sealKeys.issueNew`, `activeKid`, `retire` | `false`, `""`, `[]` | The seal key rotation, one upgrade per step; see the guide's "Rotate secrets and the seal key" | +| `platform.authSecretPrevious.existingSecret`, `key` | `""`, `auth-secret-previous` | During a rotation of Platform's `AUTH_SECRET`, a Secret with the previous value | +| `global.compatibility.openshift.adaptSecurityContext` | `auto` | `auto` leaves the pods' uids to OpenShift's SCC when the cluster is OpenShift, `force` always, `disabled` never; Studio reads it too | +| `imagePullSecrets` | `[]` | For every pod of the chart (Studio has its own under `studio:`) | +| `studio.*` | governance defaults | The Studio chart's values; repeat `hosts.studio` in `studio.ingress` and the issuer in `studio.config.oidcIssuer` | + +The chart refuses to render, naming the value, when a required value is missing or two copies of a host or the issuer disagree. + +## Tests + +- `installer/test/unit/kubernetes/chart.test.ts` renders the chart with `helm template` and holds it to the installer: render guards, the installation document against the schema, the release manifest's pins, Studio's and Keycloak's environments against the installer's renderers, the bootstrap Role against every object the job writes, and the pods' security. +- `tests/e2e/run-e2e.sh` installs on a local kind cluster and runs the browser suite of `installer/e2e/kubernetes`. +- `tests/e2e/run-openshift.sh` does the same on OpenShift, in the namespace of the current `oc login`, under the `restricted-v2` SCC. +- `tests/e2e/run-rotation.sh` rotates the seal key and the generated secrets; `tests/e2e/run-resilience.sh` kills pods and restarts the node; `tests/e2e/run-soak.sh` runs one installation for hours. +- `tests/e2e/run-restore.sh` backs up, deletes the namespace and restores; `tests/e2e/run-offline.sh` installs the packaged archive from an image mirror with no network for Helm. diff --git a/charts/libredb-platform/files/libredb-jobs.cjs.gz b/charts/libredb-platform/files/libredb-jobs.cjs.gz new file mode 100644 index 0000000000000000000000000000000000000000..6fb11e312313908e356356207feb510c48c5e512 GIT binary patch literal 173137 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

i8V47dgi}NY3Zy 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�Ay=>rvNn~io*;K32pf8v)dEpziAPpMGEg* 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 literal 0 HcmV?d00001 diff --git a/charts/libredb-platform/templates/NOTES.txt b/charts/libredb-platform/templates/NOTES.txt new file mode 100644 index 0000000..43946bb --- /dev/null +++ b/charts/libredb-platform/templates/NOTES.txt @@ -0,0 +1,47 @@ +LibreDB Platform {{ .Chart.AppVersion }} is installing in namespace {{ .Release.Namespace }}. + + Platform https://{{ .Values.hosts.platform }} + Studio https://{{ .Values.hosts.studio }} + Keycloak {{ include "libredb-platform.issuer" . }} + +Watch it come up; Platform turns ready once its migrate job has run: + + kubectl -n {{ .Release.Namespace }} get pods,jobs + kubectl -n {{ .Release.Namespace }} logs deploy/libredb-platform | grep -i licen + +{{- if eq .Values.tls.mode "generated" }} + +The internal CA issued the ingress certificates. Import it into the trust store of every workstation that opens the +three URLs (it can sign a certificate for any name, so import it only where the cluster's operators are trusted): + + kubectl -n {{ .Release.Namespace }} get secret libredb-tls-platform -o jsonpath='{.data.ca\.crt}' | base64 -d > libredb-ca.crt +{{- end }} +{{- if include "libredb-platform.managedKeycloak" . }} + +First sign-in: open Platform, choose Continue with SSO and sign in as {{ .Values.keycloak.firstAdmin.username }} with +its temporary password; Keycloak then asks for a new password and a TOTP device. + + kubectl -n {{ .Release.Namespace }} get secret libredb-keycloak-config -o jsonpath='{.data.first-admin-password}' | base64 -d; echo + +{{ .Values.keycloak.userManager.username }} manages the realm's users in Keycloak's console, which is not exposed by the ingress: + + kubectl -n {{ .Release.Namespace }} get secret libredb-keycloak-config -o jsonpath='{.data.user-manager-password}' | base64 -d; echo + kubectl -n {{ .Release.Namespace }} port-forward svc/keycloak 18080:8080 + # then open http://127.0.0.1:18080/admin/{{ .Values.keycloak.realm }}/console/ +{{- end }} +{{- if eq .Values.postgresql.mode "external" }} + +The external PostgreSQL server needs its one-time provisioning by a DBA. The script holds SCRAM verifiers only: + + kubectl -n {{ .Release.Namespace }} get secret libredb-postgres-dba-script -o jsonpath='{.data.provision\.sql}' | base64 -d > libredb-provision.sql + psql "host={{ .Values.postgresql.external.host }} port={{ .Values.postgresql.external.port }} dbname=postgres user= sslmode=verify-full sslrootcert=" -f libredb-provision.sql + +The migrate job retries until the roles exist. +{{- end }} + +BACK UP THE GENERATED SECRETS NOW. Secret libredb-secrets holds the seal keys that open every database credential +Platform stores; it outlives `helm uninstall` on purpose, and a lost key cannot be recovered: + + kubectl -n {{ .Release.Namespace }} get secret libredb-secrets libredb-tls libredb-installer-state -o yaml > libredb-secrets-backup.yaml + +A full backup set adds a dump of each database; "Back up and restore" in docs/deploy/install-kubernetes.md has both. diff --git a/charts/libredb-platform/templates/_helpers.tpl b/charts/libredb-platform/templates/_helpers.tpl new file mode 100644 index 0000000..1ae1d8d --- /dev/null +++ b/charts/libredb-platform/templates/_helpers.tpl @@ -0,0 +1,367 @@ +{{/* +Names. Every object has a fixed name, like the installer's Kubernetes constants (installer/src/kubernetes/names.ts), +so a namespace holds one installation; installer/test/unit/kubernetes/chart.test.ts holds the bootstrap Role's names +to the objects the bootstrap job writes. +*/}} + +{{- define "libredb-platform.labels" -}} +app.kubernetes.io/name: libredb-platform +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: libredb +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +{{- end }} + +{{/* Selector labels of one component: platform, postgres, keycloak or a job. */}} +{{- define "libredb-platform.selectorLabels" -}} +app.kubernetes.io/name: libredb-platform +app.kubernetes.io/instance: {{ .root.Release.Name }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{- define "libredb-platform.image" -}} +{{- printf "%s:%s" .repository .tag }} +{{- end }} + +{{- define "libredb-platform.managedPostgres" -}} +{{- if eq .Values.postgresql.mode "managed" }}true{{ end }} +{{- end }} + +{{- define "libredb-platform.managedKeycloak" -}} +{{- if eq .Values.keycloak.mode "managed" }}true{{ end }} +{{- end }} + +{{/* Whether the chart runs the keycloak-configure job: a managed Keycloak, or an external one with provisioning installer. */}} +{{- define "libredb-platform.keycloakJob" -}} +{{- if or (include "libredb-platform.managedKeycloak" .) (eq .Values.keycloak.external.provisioning "installer") }}true{{ end }} +{{- end }} + +{{- define "libredb-platform.issuer" -}} +{{- if include "libredb-platform.managedKeycloak" . -}} +{{- printf "https://%s/realms/%s" .Values.hosts.auth .Values.keycloak.realm }} +{{- else -}} +{{- printf "%s/realms/%s" (trimSuffix "/" .Values.keycloak.external.baseUrl) .Values.keycloak.realm }} +{{- end }} +{{- end }} + +{{/* Where the bootstrap job finds the operator's inputs, as the installation document names them. */}} +{{- define "libredb-platform.inputDir" -}}/etc/libredb/inputs{{- end }} + +{{/* +The installation document: the Helm values as a LibreDBInstallation of mode kubernetes, which the jobs validate with +the installer's schema (installer/src/config/schema.ts). It holds no secret, only the files that hold them. +*/}} +{{- define "libredb-platform.installation" -}} +{{- $in := include "libredb-platform.inputDir" . -}} +{{- $tls := dict "mode" "generated" -}} +{{- if eq .Values.tls.mode "secrets" -}} +{{- $tls = dict "mode" "external" "caBundleFile" (printf "%s/ca/tls-ca.crt" $in) "external" (dict "trustedProxies" (list "private_ranges")) -}} +{{- end -}} +{{- $postgres := dict "mode" "managed" -}} +{{- if eq .Values.postgresql.mode "external" -}} +{{- $ext := dict "host" .Values.postgresql.external.host "port" (int .Values.postgresql.external.port) "provisioning" "manual" -}} +{{- if .Values.postgresql.external.caBundle }}{{ $_ := set $ext "caBundleFile" (printf "%s/ca/postgres-ca.crt" $in) }}{{ end -}} +{{- $postgres = dict "mode" "external" "external" $ext -}} +{{- end -}} +{{- $kc := .Values.keycloak -}} +{{- $account := dict "firstAdmin" (dict "username" $kc.firstAdmin.username) "userManager" (dict "username" $kc.userManager.username) -}} +{{- if not (kindIs "invalid" $kc.firstAdmin.enabled) }}{{ $_ := set $account.firstAdmin "enabled" $kc.firstAdmin.enabled }}{{ end -}} +{{- if not (kindIs "invalid" $kc.userManager.enabled) }}{{ $_ := set $account.userManager "enabled" $kc.userManager.enabled }}{{ end -}} +{{- $keycloak := dict "mode" $kc.mode "realm" $kc.realm "firstAdmin" $account.firstAdmin "userManager" $account.userManager -}} +{{- if eq $kc.mode "external" -}} +{{- $x := $kc.external -}} +{{- $ext := dict "baseUrl" $x.baseUrl "provisioning" $x.provisioning -}} +{{- if $x.adminUrl }}{{ $_ := set $ext "adminUrl" $x.adminUrl }}{{ end -}} +{{- if $x.caBundle }}{{ $_ := set $ext "caBundleFile" (printf "%s/ca/keycloak-ca.crt" $in) }}{{ end -}} +{{- if eq $x.provisioning "installer" -}} +{{- $_ := set $ext "adminRealm" $x.adminRealm -}} +{{- if $x.adminClientId -}} +{{- $_ := set $ext "adminClientId" $x.adminClientId -}} +{{- $_ := set $ext "adminClientSecretFile" (printf "%s/keycloak-admin/admin-client-secret" $in) -}} +{{- end -}} +{{- if $x.adminUser -}} +{{- $_ := set $ext "adminUser" $x.adminUser -}} +{{- $_ := set $ext "adminPasswordFile" (printf "%s/keycloak-admin/admin-password" $in) -}} +{{- end -}} +{{- else -}} +{{- $_ := set $ext "studioClientSecretFile" (printf "%s/keycloak-clients/studio-client-secret" $in) -}} +{{- $_ := set $ext "platformClientSecretFile" (printf "%s/keycloak-clients/platform-client-secret" $in) -}} +{{- end -}} +{{- $_ := set $keycloak "external" $ext -}} +{{- end -}} +{{- $platform := dict "organizationName" .Values.platform.organizationName "licenseFile" (printf "%s/license/license" $in) -}} +{{- if .Values.platform.authSecretPrevious.existingSecret }}{{ $_ := set $platform "authSecretPreviousFile" (printf "%s/auth-secret-previous/auth-secret-previous" $in) }}{{ end -}} +{{- if not (kindIs "invalid" .Values.platform.queryLogRetentionDays) }}{{ $_ := set $platform "queryLogRetentionDays" (int .Values.platform.queryLogRetentionDays) }}{{ end -}} +{{- $doc := dict "apiVersion" "install.libredb.org/v1" "kind" "LibreDBInstallation" "metadata" (dict "name" .Values.installation.name) "mode" "kubernetes" "endpoints" (dict "style" "hostnames" "hosts" .Values.hosts) "tls" $tls "postgres" $postgres "keycloak" $keycloak "platform" $platform "kubernetes" (dict "namespace" .Release.Namespace) -}} +{{- if .Values.network.httpsProxy }}{{ $_ := set $doc "network" (dict "httpsProxy" .Values.network.httpsProxy) }}{{ end -}} +{{- $seal := .Values.platform.sealKeys -}} +{{- $sealKeys := dict "issueNew" $seal.issueNew "retire" (include "libredb-platform.retiredKids" . | fromJsonArray) -}} +{{- if $seal.activeKid }}{{ $_ := set $sealKeys "activeKid" (include "libredb-platform.kid" (dict "value" $seal.activeKid "name" "platform.sealKeys.activeKid")) }}{{ end -}} +{{- $_ := set $doc "bridge" (dict "sealKeys" $sealKeys) -}} +{{- toPrettyJson $doc }} +{{- end }} + +{{/* A seal key id, 16 lowercase hex digits, or a failed render that names the value: dict "value" "name". */}} +{{- define "libredb-platform.kid" -}} +{{- if not (regexMatch "^[0-9a-f]{16}$" (toString .value)) }}{{ fail (printf "%s: %q is no seal key id, which is 16 lowercase hex digits" .name (toString .value)) }}{{ end -}} +{{- .value -}} +{{- end }} + +{{/* platform.sealKeys.retire as a JSON array, each kid checked, since the jobs' SQL quotes them as literals. */}} +{{- define "libredb-platform.retiredKids" -}} +{{- $kids := list -}} +{{- range $index, $kid := .Values.platform.sealKeys.retire -}} +{{- $kids = append $kids (include "libredb-platform.kid" (dict "value" $kid "name" (printf "platform.sealKeys.retire[%d]" $index))) -}} +{{- end -}} +{{- toJson $kids -}} +{{- end }} + +{{/* +The installer's envelopeUsesScript (installer/src/secrets/seal-rotation.ts) for platform.sealKeys.retire, which the +bootstrap job's seal-envelopes init container runs as libredb_platform; installer/test/unit/kubernetes/chart.test.ts +holds the two to the same text. +*/}} +{{- define "libredb-platform.sealEnvelopesSql" -}} +{{- $quoted := list -}} +{{- range (include "libredb-platform.retiredKids" . | fromJsonArray) }}{{ $quoted = append $quoted (printf "'%s'" .) }}{{ end -}} +SELECT 'libredb-result envelope-uses ' || ((SELECT coalesce(jsonb_agg(jsonb_build_object('kid', e.kid, 'connectionId', e.connection_id::text, 'name', e.name) ORDER BY e.kid, e.name), '[]'::jsonb) FROM (SELECT connection_id, name, split_part(read_secret, '.', 2) AS kid FROM bridge.connections_v1 UNION SELECT connection_id, name, split_part(write_secret, '.', 2) FROM bridge.connections_v1 WHERE write_secret IS NOT NULL) e WHERE e.kid IN ({{ join ", " $quoted }})))::text; +{{ end }} + +{{/* The installer's deleteCanaryScript for each retired kid, which the restart job runs once the release applied. */}} +{{- define "libredb-platform.sealCanarySql" -}} +{{- range (include "libredb-platform.retiredKids" . | fromJsonArray) -}} +DELETE FROM platform.seal_canary WHERE kid = '{{ . }}'; +{{ end -}} +{{- end }} + +{{/* +An init container that runs one SQL file of the chart as libredb_platform over verified TLS, with the role's password +from Secret libredb-platform-migrate-env in PGPASSWORD, never on a command line: dict "root" . "name" "file" "output". +*/}} +{{- define "libredb-platform.platformSqlContainer" -}} +- name: {{ .name }} + image: {{ include "libredb-platform.image" .root.Values.postgresql.managed.image }} + imagePullPolicy: {{ .root.Values.postgresql.managed.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + set -eu + rest="${DATABASE_URL#postgresql://libredb_platform:}" + export PGPASSWORD="${rest%%@*}" + address="${rest#*@}" + address="${address%%/*}" + conninfo="host=${address%:*} port=${address##*:} dbname=libredb user=libredb_platform sslmode=verify-full sslrootcert=/etc/libredb/ca/ca.crt connect_timeout=10" + psql "$conninfo" -X -q -A -t -v ON_ERROR_STOP=1 -f {{ .file }}{{ with .output }} > {{ . }}{{ end }} + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: libredb-platform-migrate-env + key: DATABASE_URL + securityContext: + {{- include "libredb-platform.containerSecurity" .root | nindent 4 }} + resources: + {{- toYaml .root.Values.jobs.resources | nindent 4 }} + volumeMounts: + - name: installation-ca + mountPath: /etc/libredb/ca + readOnly: true + - name: chart + mountPath: /etc/libredb/chart + readOnly: true + - name: tmp + mountPath: /tmp +{{- end }} + +{{- define "libredb-platform.chartSettings" -}} +{{- /* No Helm or Argo CD tracking label (app.kubernetes.io/instance): a tool that tracked these objects would prune them. */}} +{{- toPrettyJson (dict "studioSecretName" .Values.studio.secrets.existingSecret "labels" (dict "app.kubernetes.io/part-of" "libredb" "app.kubernetes.io/managed-by" "libredb-bootstrap" "install.libredb.org/installation" .Values.installation.name)) }} +{{- end }} + +{{/* A short hash that names a job after what it runs, so a changed job is a new Job and an unchanged one stays done. */}} +{{- define "libredb-platform.hash" -}} +{{- . | sha256sum | trunc 8 }} +{{- end }} + +{{/* The restricted security context of every LibreDB container (uid 1001, as the installer's compose file). */}} +{{- define "libredb-platform.containerSecurity" -}} +allowPrivilegeEscalation: false +readOnlyRootFilesystem: true +runAsNonRoot: true +capabilities: + drop: ["ALL"] +seccompProfile: + type: RuntimeDefault +{{- end }} + +{{/* +Whether to leave out the pods' fixed uids for OpenShift, whose restricted-v2 SCC assigns runAsUser and fsGroup from the +namespace's range and rejects any other: global.compatibility.openshift.adaptSecurityContext, the value the Studio +chart reads too, "auto" when the API server serves security.openshift.io/v1, "force" always, "disabled" never. +*/}} +{{- define "libredb-platform.adaptOpenShift" -}} +{{- $mode := dig "compatibility" "openshift" "adaptSecurityContext" "auto" (.Values.global | default dict) }} +{{- if or (eq $mode "force") (and (eq $mode "auto") (.Capabilities.APIVersions.Has "security.openshift.io/v1")) }} +{{- true }} +{{- end }} +{{- end }} + +{{/* The pod security context, with the image's uid unless OpenShift assigns one: dict "root" . "uid" . */}} +{{- define "libredb-platform.podSecurity" -}} +runAsNonRoot: true +{{- if not (include "libredb-platform.adaptOpenShift" .root) }} +runAsUser: {{ .uid }} +runAsGroup: {{ .uid }} +fsGroup: {{ .uid }} +{{- end }} +seccompProfile: + type: RuntimeDefault +{{- end }} + +{{/* +Where a pod of the Platform image may run: platform.nodeSelector and platform.tolerations, with kubernetes.io/arch amd64 +unless platform.nodeSelector names another, since the Platform image is built for linux/amd64 alone. Platform and the +jobs that run its image share it, so a job never lands on a node the image cannot run on. +*/}} +{{- define "libredb-platform.platformScheduling" -}} +nodeSelector: + {{- toYaml (merge (dict) .Values.platform.nodeSelector (dict "kubernetes.io/arch" "amd64")) | nindent 2 }} +{{- with .Values.platform.tolerations }} +tolerations: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{/* +The node command of a chart job: the gzipped bundle decompressed into /tmp, then run with the cluster CA and, for the +keycloak job, the installation's CA bundle trusted. +*/}} +{{- define "libredb-platform.jobCommand" -}} +- /bin/sh +- -c +- | + set -eu + cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt {{ if .withBundle }}/etc/libredb/ca/ca.crt {{ end }}> /tmp/trust.pem + node -e 'const z=require("zlib"),f=require("fs");f.writeFileSync("/tmp/libredb-jobs.cjs",z.gunzipSync(f.readFileSync("/etc/libredb/jobs/libredb-jobs.cjs.gz")))' + NODE_EXTRA_CA_CERTS=/tmp/trust.pem exec node /tmp/libredb-jobs.cjs {{ .command }} +{{- end }} + +{{/* +An init container that waits until the installation's PostgreSQL server accepts connections, so a job that starts +beside a server that is still starting waits instead of failing. pg_isready checks the protocol, without credentials. +*/}} +{{- define "libredb-platform.waitForPostgres" -}} +- name: wait-for-postgres + image: {{ include "libredb-platform.image" .Values.postgresql.managed.image }} + imagePullPolicy: {{ .Values.postgresql.managed.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + until pg_isready -q -t 5 -h "$PGHOST" -p "$PGPORT"; do echo "waiting for PostgreSQL at $PGHOST:$PGPORT"; sleep 3; done + env: + # libpq looks the OS user up when PGUSER is unset, which fails for a uid the image has no passwd entry for. + - name: PGUSER + value: postgres + - name: PGHOST + value: {{ if include "libredb-platform.managedPostgres" . }}libredb-postgres{{ else }}{{ .Values.postgresql.external.host | quote }}{{ end }} + - name: PGPORT + value: {{ if include "libredb-platform.managedPostgres" . }}"5432"{{ else }}{{ .Values.postgresql.external.port | quote }}{{ end }} + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 4 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 4 }} +{{- end }} + +{{/* +Render guards: every combination the installation could not run with fails here, naming the value, before anything +reaches the cluster. The installer schema checks the rest in the bootstrap job. +*/}} +{{- define "libredb-platform.guards" -}} +{{- range $endpoint := list "platform" "studio" "auth" }} +{{- if not (get $.Values.hosts $endpoint) }} +{{- fail (printf "hosts.%s is required: the public host of %s" $endpoint $endpoint) }} +{{- end }} +{{- end }} +{{- if not .Values.platform.organizationName }} +{{- fail "platform.organizationName is required: the name of the installation's one tenant" }} +{{- end }} +{{- if and (not .Values.platform.license.value) (not .Values.platform.license.existingSecret) }} +{{- fail "platform.license is required: every install needs a license from LibreDB (info@libredb.org); pass it with --set-file platform.license.value=, or name a Secret in platform.license.existingSecret" }} +{{- end }} +{{- if and .Values.platform.license.value (not (regexMatch "^ldl1\\.[0-9a-f]{16}\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$" (trim .Values.platform.license.value))) }} +{{- fail "platform.license.value must hold the license token alone, one line of the form ldl1..." }} +{{- end }} +{{- if not (has .Values.tls.mode (list "generated" "secrets")) }} +{{- fail (printf "tls.mode must be generated or secrets, not %q" .Values.tls.mode) }} +{{- end }} +{{- if eq .Values.tls.mode "secrets" }} +{{- range $endpoint := list "platform" "studio" }} +{{- if not (get $.Values.tls.secrets $endpoint) }} +{{- fail (printf "tls.mode secrets needs tls.secrets.%s, the TLS Secret of hosts.%s" $endpoint $endpoint) }} +{{- end }} +{{- end }} +{{- if and (include "libredb-platform.managedKeycloak" .) (not .Values.tls.secrets.auth) }} +{{- fail "tls.mode secrets with a managed Keycloak needs tls.secrets.auth, the TLS Secret of hosts.auth" }} +{{- end }} +{{- if not .Values.tls.caBundle }} +{{- fail "tls.mode secrets needs tls.caBundle, the PEM of the CA that issued the ingress certificates, which every pod trusts" }} +{{- end }} +{{- end }} +{{- if not (has .Values.postgresql.mode (list "managed" "external")) }} +{{- fail (printf "postgresql.mode must be managed or external, not %q" .Values.postgresql.mode) }} +{{- end }} +{{- if and (eq .Values.postgresql.mode "external") (not .Values.postgresql.external.host) }} +{{- fail "postgresql.mode external needs postgresql.external.host, the DNS name its certificate covers" }} +{{- end }} +{{- if not (has .Values.keycloak.mode (list "managed" "external")) }} +{{- fail (printf "keycloak.mode must be managed or external, not %q" .Values.keycloak.mode) }} +{{- end }} +{{- if eq .Values.keycloak.mode "external" }} +{{- $x := .Values.keycloak.external }} +{{- if not $x.baseUrl }} +{{- fail "keycloak.mode external needs keycloak.external.baseUrl" }} +{{- end }} +{{- if not (has $x.provisioning (list "installer" "manual")) }} +{{- fail (printf "keycloak.external.provisioning must be installer or manual, not %q" $x.provisioning) }} +{{- end }} +{{- if and (eq $x.provisioning "installer") (not $x.adminSecret) }} +{{- fail "keycloak.external.provisioning installer needs keycloak.external.adminSecret, the Secret with the admin credential" }} +{{- end }} +{{- if and (eq $x.provisioning "installer") (ne (len (compact (list $x.adminClientId $x.adminUser))) 1) }} +{{- fail "keycloak.external.provisioning installer needs exactly one of keycloak.external.adminClientId and keycloak.external.adminUser" }} +{{- end }} +{{- if and (eq $x.provisioning "manual") (not $x.clientSecretsSecret) }} +{{- fail "keycloak.external.provisioning manual needs keycloak.external.clientSecretsSecret, with the keys studio-client-secret and platform-client-secret" }} +{{- end }} +{{- end }} +{{- if and .Values.network.httpsProxy (regexMatch "(?i)^[a-z][a-z0-9+.-]*://[^/]*@" .Values.network.httpsProxy) }} +{{- fail "network.httpsProxy must not hold credentials: the installation document that holds it is a ConfigMap" }} +{{- end }} +{{- if .Values.studio.enabled }} +{{- $issuer := include "libredb-platform.issuer" . }} +{{- if ne .Values.studio.config.oidcIssuer $issuer }} +{{- fail (printf "studio.config.oidcIssuer must be %s, the issuer of hosts.auth and keycloak.realm, not %q" $issuer .Values.studio.config.oidcIssuer) }} +{{- end }} +{{- if ne (.Values.studio.fullnameOverride | toString) "libredb-studio" }} +{{- fail "studio.fullnameOverride must stay libredb-studio: the restart job of the chart names Studio's Deployment so" }} +{{- end }} +{{- if ne .Values.studio.secrets.existingSecret "libredb-studio-env" }} +{{- fail "studio.secrets.existingSecret must stay libredb-studio-env: the bootstrap job writes Studio's secrets there" }} +{{- end }} +{{- if .Values.studio.ingress.enabled }} +{{- $hosts := list }} +{{- range .Values.studio.ingress.hosts }}{{ $hosts = append $hosts .host }}{{ end }} +{{- if ne (toJson $hosts) (toJson (list .Values.hosts.studio)) }} +{{- fail (printf "studio.ingress.hosts must hold exactly one host, hosts.studio (%s)" .Values.hosts.studio) }} +{{- end }} +{{- $tlsSecret := ternary "libredb-tls-studio" .Values.tls.secrets.studio (eq .Values.tls.mode "generated") }} +{{- $tls := .Values.studio.ingress.tls | default list }} +{{- if or (ne (len $tls) 1) (ne (get (first $tls | default dict) "secretName" | default "") $tlsSecret) (ne (toJson (get (first $tls | default dict) "hosts" | default list)) (toJson (list .Values.hosts.studio))) }} +{{- fail (printf "studio.ingress.tls must hold one entry with secretName %s and hosts [%s]: the TLS Secret of hosts.studio under tls.mode %s" $tlsSecret .Values.hosts.studio .Values.tls.mode) }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/libredb-platform/templates/bootstrap-hook.yaml b/charts/libredb-platform/templates/bootstrap-hook.yaml new file mode 100644 index 0000000..f4e8132 --- /dev/null +++ b/charts/libredb-platform/templates/bootstrap-hook.yaml @@ -0,0 +1,269 @@ +{{- include "libredb-platform.guards" . }} +{{- /* +The bootstrap job (installer/src/kubernetes/bootstrap.ts), a pre-install and pre-upgrade hook: it generates the +secrets, the seal keys and the internal CA once, and writes every Secret and ConfigMap the workloads mount, before any +of them starts. Its ServiceAccount, Role and inputs exist only while it runs. The Secrets it writes carry no Helm +ownership, so they outlive `helm uninstall`: losing the seal keys loses every sealed database credential. +*/}} +{{- /* Also before a rollback, whose bootstrap refuses an older release than the installation ran (checkRelease). */}} +{{- $hook := dict "helm.sh/hook" "pre-install,pre-upgrade,pre-rollback" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded" }} +{{- $managedPg := include "libredb-platform.managedPostgres" . }} +{{- $managedKc := include "libredb-platform.managedKeycloak" . }} +{{- $secrets := list "libredb-secrets" "libredb-installer-state" "libredb-tls" "libredb-platform-env" "libredb-platform-migrate-env" "libredb-keycloak-config" "libredb-studio-network-env" .Values.studio.secrets.existingSecret }} +{{- if $managedPg }}{{ $secrets = concat $secrets (list "libredb-postgres-superuser" "libredb-postgres-tls" "libredb-postgres-provision") }}{{ end }} +{{- if not $managedPg }}{{ $secrets = append $secrets "libredb-postgres-dba-script" }}{{ end }} +{{- if $managedKc }}{{ $secrets = append $secrets "libredb-keycloak-env" }}{{ end }} +{{- if eq .Values.tls.mode "generated" }}{{ $secrets = concat $secrets (list "libredb-tls-platform" "libredb-tls-studio") }}{{ if $managedKc }}{{ $secrets = append $secrets "libredb-tls-auth" }}{{ end }}{{ end }} +{{- $configMaps := list "libredb-ca" "libredb-jobs" }} +{{- if $managedPg }}{{ $configMaps = append $configMaps "libredb-postgres-config" }}{{ end }} +{{- if $managedKc }}{{ $configMaps = append $configMaps "libredb-keycloak-edge" }}{{ end }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: libredb-bootstrap + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-30" +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: libredb-bootstrap + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-30" +rules: + # create cannot be limited by name; reads and replaces are limited to the job's own objects. + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: ["create"] + - apiGroups: [""] + resources: ["secrets"] + resourceNames: {{ toJson $secrets }} + verbs: ["get", "update"] + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: {{ toJson $configMaps }} + verbs: ["get", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: libredb-bootstrap + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-30" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: libredb-bootstrap +subjects: + - kind: ServiceAccount + name: libredb-bootstrap + namespace: {{ .Release.Namespace }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: libredb-bootstrap-chart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-20" +data: + installation.json: | + {{- include "libredb-platform.installation" . | nindent 4 }} + chart.json: | + {{- include "libredb-platform.chartSettings" . | nindent 4 }} + {{- if .Values.platform.sealKeys.retire }} + seal-envelopes.sql: | + {{- include "libredb-platform.sealEnvelopesSql" . | nindent 4 }} + {{- end }} + {{- with .Values.tls.caBundle }} + tls-ca.crt: {{ . | quote }} + {{- end }} + {{- with .Values.postgresql.external.caBundle }} + postgres-ca.crt: {{ . | quote }} + {{- end }} + {{- with .Values.keycloak.external.caBundle }} + keycloak-ca.crt: {{ . | quote }} + {{- end }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: libredb-bootstrap-jobs + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-20" +binaryData: + libredb-jobs.cjs.gz: {{ .Files.Get "files/libredb-jobs.cjs.gz" | b64enc }} +{{- if .Values.platform.license.value }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: libredb-bootstrap-license + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-20" +type: Opaque +stringData: + license: {{ trim .Values.platform.license.value | quote }} +{{- end }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: libredb-bootstrap + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: bootstrap + annotations: + {{- toYaml $hook | nindent 4 }} + helm.sh/hook-weight: "-10" +spec: + # A refusal a retry cannot change (exit 2 invalid installation, 3 failed precondition, 6 unsafe change, as the + # installer's exit codes) fails the Job at once, so Helm reports it instead of waiting out the retries. + podFailurePolicy: + rules: + - action: FailJob + onExitCodes: + containerName: bootstrap + operator: In + values: [2, 3, 6] + backoffLimit: {{ .Values.jobs.backoffLimit }} + activeDeadlineSeconds: {{ .Values.jobs.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "bootstrap") | nindent 8 }} + spec: + serviceAccountName: libredb-bootstrap + automountServiceAccountToken: true + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1001) | nindent 8 }} + {{- include "libredb-platform.platformScheduling" $ | nindent 6 }} + {{- if .Values.platform.sealKeys.retire }} + # A retirement needs the connections each retired kid still seals, which the bootstrap job reads from /tmp. + initContainers: + {{- include "libredb-platform.platformSqlContainer" (dict "root" . "name" "seal-envelopes" "file" "/etc/libredb/chart/seal-envelopes.sql" "output" "/tmp/seal-envelopes.out") | nindent 8 }} + {{- end }} + containers: + - name: bootstrap + image: {{ include "libredb-platform.image" .Values.platform.image }} + imagePullPolicy: {{ .Values.platform.image.pullPolicy }} + command: + {{- include "libredb-platform.jobCommand" (dict "command" "bootstrap" "withBundle" false) | nindent 12 }} + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: chart + mountPath: /etc/libredb/chart + readOnly: true + - name: jobs + mountPath: /etc/libredb/jobs + readOnly: true + - name: inputs-ca + mountPath: {{ include "libredb-platform.inputDir" . }}/ca + readOnly: true + - name: license + mountPath: {{ include "libredb-platform.inputDir" . }}/license + readOnly: true + {{- if .Values.platform.authSecretPrevious.existingSecret }} + - name: auth-secret-previous + mountPath: {{ include "libredb-platform.inputDir" . }}/auth-secret-previous + readOnly: true + {{- end }} + {{- if and (eq .Values.keycloak.mode "external") (eq .Values.keycloak.external.provisioning "manual") }} + - name: keycloak-clients + mountPath: {{ include "libredb-platform.inputDir" . }}/keycloak-clients + readOnly: true + {{- end }} + - name: tmp + mountPath: /tmp + volumes: + - name: chart + configMap: + name: libredb-bootstrap-chart + items: + - key: installation.json + path: installation.json + - key: chart.json + path: chart.json + {{- if .Values.platform.sealKeys.retire }} + - key: seal-envelopes.sql + path: seal-envelopes.sql + {{- end }} + - name: jobs + configMap: + name: libredb-bootstrap-jobs + - name: inputs-ca + configMap: + name: libredb-bootstrap-chart + optional: true + items: + {{- if .Values.tls.caBundle }} + - key: tls-ca.crt + path: tls-ca.crt + {{- end }} + {{- if .Values.postgresql.external.caBundle }} + - key: postgres-ca.crt + path: postgres-ca.crt + {{- end }} + {{- if .Values.keycloak.external.caBundle }} + - key: keycloak-ca.crt + path: keycloak-ca.crt + {{- end }} + - key: chart.json + path: .chart.json + - name: license + secret: + secretName: {{ .Values.platform.license.existingSecret | default "libredb-bootstrap-license" }} + items: + - key: {{ if .Values.platform.license.existingSecret }}{{ .Values.platform.license.key }}{{ else }}license{{ end }} + path: license + {{- with .Values.platform.authSecretPrevious.existingSecret }} + - name: auth-secret-previous + secret: + secretName: {{ . }} + items: + - key: {{ $.Values.platform.authSecretPrevious.key }} + path: auth-secret-previous + {{- end }} + {{- if .Values.platform.sealKeys.retire }} + - name: installation-ca + configMap: + name: libredb-ca + {{- end }} + {{- if and (eq .Values.keycloak.mode "external") (eq .Values.keycloak.external.provisioning "manual") }} + - name: keycloak-clients + secret: + secretName: {{ .Values.keycloak.external.clientSecretsSecret }} + items: + - key: studio-client-secret + path: studio-client-secret + - key: platform-client-secret + path: platform-client-secret + {{- end }} + - name: tmp + emptyDir: {} diff --git a/charts/libredb-platform/templates/jobs.yaml b/charts/libredb-platform/templates/jobs.yaml new file mode 100644 index 0000000..9f162a7 --- /dev/null +++ b/charts/libredb-platform/templates/jobs.yaml @@ -0,0 +1,377 @@ +{{- /* +The jobs that run after the bootstrap hook: Platform's migrate job and the keycloak-configure job. Each is named by a +hash of the release revision and what it runs, so every install and upgrade runs it again, as every installer apply +does, which is safe because both converge, and a job that failed is retried by the next upgrade; Helm removes the +previous revision's Job. Each retries until what it needs is up: the migrate job until PostgreSQL is provisioned, the keycloak +job until Keycloak answers. Platform's readiness (/api/health/ready) stays 503 until the migration ran. +*/}} +{{- $migrate := dict "revision" .Release.Revision "image" (include "libredb-platform.image" .Values.platform.image) "installation" (include "libredb-platform.installation" .) }} +apiVersion: batch/v1 +kind: Job +metadata: + name: libredb-platform-migrate-{{ include "libredb-platform.hash" (toJson $migrate) }} + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: platform-migrate + annotations: + # Argo CD renders with helm template, where every revision is 1: as a Sync hook replaced on each sync, the job + # runs again on every sync as on every helm upgrade. Helm ignores these annotations. + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + # The job retries until what it needs is up, so it gets more attempts than the bootstrap hook. + backoffLimit: {{ add .Values.jobs.backoffLimit 4 }} + activeDeadlineSeconds: {{ .Values.jobs.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "platform-migrate") | nindent 8 }} + spec: + automountServiceAccountToken: false + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1001) | nindent 8 }} + {{- include "libredb-platform.platformScheduling" $ | nindent 6 }} + initContainers: + # Waits until libredb_platform can sign in over verified TLS, which happens once the server is provisioned: + # by the provisioning job of a managed server, by the DBA's script on an external one. + - name: wait-for-provisioning + image: {{ include "libredb-platform.image" .Values.postgresql.managed.image }} + imagePullPolicy: {{ .Values.postgresql.managed.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + # The password goes to libpq in PGPASSWORD, never on a command line; the bootstrap job generates it in + # base64url, which a URL carries unescaped. + rest="${DATABASE_URL#postgresql://libredb_platform:}" + export PGPASSWORD="${rest%%@*}" + address="${rest#*@}" + address="${address%%/*}" + conninfo="host=${address%:*} port=${address##*:} dbname=libredb user=libredb_platform sslmode=verify-full sslrootcert=/etc/libredb/ca/ca.crt connect_timeout=5" + until psql "$conninfo" -X -A -t -q -c 'SELECT 1' >/dev/null 2>&1; do echo "waiting until PostgreSQL is provisioned for libredb_platform"; sleep 3; done + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: libredb-platform-migrate-env + key: DATABASE_URL + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + containers: + - name: migrate + image: {{ include "libredb-platform.image" .Values.platform.image }} + imagePullPolicy: {{ .Values.platform.image.pullPolicy }} + args: ["migrate"] + envFrom: + # The only place MIGRATE_DATABASE_URL, the owner role's URL, lives. + - secretRef: + name: libredb-platform-migrate-env + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: ca + configMap: + name: libredb-ca + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: libredb-chart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +data: + installation.json: | + {{- include "libredb-platform.installation" . | nindent 4 }} + chart.json: | + {{- include "libredb-platform.chartSettings" . | nindent 4 }} + {{- if .Values.platform.sealKeys.retire }} + seal-canary.sql: | + {{- include "libredb-platform.sealCanarySql" . | nindent 4 }} + {{- end }} +--- +{{- /* +The restart job, a post-install, post-upgrade and post-rollback hook (Argo CD: PostSync): it restarts each workload +whose Secret or ConfigMap the bootstrap job changed, once Helm applied the release's pod templates, unless its pods +already started after the change. Its ServiceAccount and Role are regular objects, which exist by then. +*/}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: libredb-restart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: libredb-restart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["libredb-installer-state"] + verbs: ["get", "update"] + - apiGroups: ["apps"] + resources: ["deployments"] + resourceNames: ["libredb-platform", "libredb-studio", "keycloak"] + verbs: ["get", "patch"] + - apiGroups: ["apps"] + resources: ["statefulsets"] + resourceNames: ["libredb-postgres"] + verbs: ["get", "patch"] + # Whether a workload's pods already started after the change. + - apiGroups: [""] + resources: ["pods"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: libredb-restart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: libredb-restart +subjects: + - kind: ServiceAccount + name: libredb-restart + namespace: {{ .Release.Namespace }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: libredb-restart + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: restart + annotations: + helm.sh/hook: post-install,post-upgrade,post-rollback + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: {{ .Values.jobs.backoffLimit }} + activeDeadlineSeconds: {{ .Values.jobs.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "restart") | nindent 8 }} + spec: + serviceAccountName: libredb-restart + automountServiceAccountToken: true + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1001) | nindent 8 }} + {{- include "libredb-platform.platformScheduling" $ | nindent 6 }} + {{- if .Values.platform.sealKeys.retire }} + # A retired kid's canary row would make every Studio heartbeat report UNKNOWN_KID for good, and Platform only + # inserts canaries; it goes before the restarts, so Studio starts without the kid and without its canary. + initContainers: + {{- include "libredb-platform.platformSqlContainer" (dict "root" . "name" "seal-canary" "file" "/etc/libredb/chart/seal-canary.sql") | nindent 8 }} + {{- end }} + containers: + - name: restart + image: {{ include "libredb-platform.image" .Values.platform.image }} + imagePullPolicy: {{ .Values.platform.image.pullPolicy }} + command: + {{- include "libredb-platform.jobCommand" (dict "command" "restart" "withBundle" false) | nindent 12 }} + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: chart + mountPath: /etc/libredb/chart + readOnly: true + - name: jobs + mountPath: /etc/libredb/jobs + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: chart + configMap: + name: libredb-chart + items: + - key: chart.json + path: chart.json + {{- if .Values.platform.sealKeys.retire }} + - key: seal-canary.sql + path: seal-canary.sql + {{- end }} + - name: jobs + configMap: + name: libredb-jobs + - name: tmp + emptyDir: {} + {{- if .Values.platform.sealKeys.retire }} + - name: installation-ca + configMap: + name: libredb-ca + {{- end }} +{{- if include "libredb-platform.keycloakJob" . }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: libredb-keycloak-configure + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: libredb-keycloak-configure + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +rules: + # The installer state, which records the Keycloak accounts the job created. + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["libredb-installer-state"] + verbs: ["get", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: libredb-keycloak-configure + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: libredb-keycloak-configure +subjects: + - kind: ServiceAccount + name: libredb-keycloak-configure + namespace: {{ .Release.Namespace }} +--- +{{- $keycloak := dict "revision" .Release.Revision "image" (include "libredb-platform.image" .Values.platform.image) "installation" (include "libredb-platform.installation" .) "jobs" (.Files.Get "files/libredb-jobs.cjs.gz" | sha256sum) }} +apiVersion: batch/v1 +kind: Job +metadata: + name: libredb-keycloak-configure-{{ include "libredb-platform.hash" (toJson $keycloak) }} + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak-configure + annotations: + # Argo CD renders with helm template, where every revision is 1: as a Sync hook replaced on each sync, the job + # runs again on every sync as on every helm upgrade. Helm ignores these annotations. + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + # A refusal a retry cannot change (exit 2 invalid installation, 3 failed precondition, 6 unsafe change, as the + # installer's exit codes) fails the Job at once, so Helm reports it instead of waiting out the retries. + podFailurePolicy: + rules: + - action: FailJob + onExitCodes: + containerName: keycloak-configure + operator: In + values: [2, 3, 6] + # The job retries until what it needs is up, so it gets more attempts than the bootstrap hook. + backoffLimit: {{ add .Values.jobs.backoffLimit 4 }} + activeDeadlineSeconds: {{ .Values.jobs.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak-configure") | nindent 8 }} + spec: + serviceAccountName: libredb-keycloak-configure + automountServiceAccountToken: true + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1001) | nindent 8 }} + {{- include "libredb-platform.platformScheduling" $ | nindent 6 }} + containers: + - name: keycloak-configure + image: {{ include "libredb-platform.image" .Values.platform.image }} + imagePullPolicy: {{ .Values.platform.image.pullPolicy }} + command: + {{- include "libredb-platform.jobCommand" (dict "command" "keycloak" "withBundle" true) | nindent 12 }} + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: chart + mountPath: /etc/libredb/chart + readOnly: true + - name: jobs + mountPath: /etc/libredb/jobs + readOnly: true + - name: keycloak-config + mountPath: /etc/libredb/keycloak-config + readOnly: true + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + {{- if eq .Values.keycloak.mode "external" }} + - name: keycloak-admin + mountPath: {{ include "libredb-platform.inputDir" . }}/keycloak-admin + readOnly: true + {{- end }} + - name: tmp + mountPath: /tmp + volumes: + - name: chart + configMap: + name: libredb-chart + # Written by the bootstrap job from its hook ConfigMap, so the release carries the bundle once. + - name: jobs + configMap: + name: libredb-jobs + - name: keycloak-config + secret: + secretName: libredb-keycloak-config + - name: ca + configMap: + name: libredb-ca + {{- if eq .Values.keycloak.mode "external" }} + - name: keycloak-admin + secret: + secretName: {{ .Values.keycloak.external.adminSecret }} + items: + {{- if .Values.keycloak.external.adminClientId }} + - key: admin-client-secret + path: admin-client-secret + {{- else }} + - key: admin-password + path: admin-password + {{- end }} + {{- end }} + - name: tmp + emptyDir: {} +{{- end }} diff --git a/charts/libredb-platform/templates/keycloak.yaml b/charts/libredb-platform/templates/keycloak.yaml new file mode 100644 index 0000000..85f0e97 --- /dev/null +++ b/charts/libredb-platform/templates/keycloak.yaml @@ -0,0 +1,220 @@ +{{- if include "libredb-platform.managedKeycloak" . }} +{{- /* +The managed Keycloak, with the environment of the installer's compose service (installer/src/render/compose.ts), and +the installer's edge as a sidecar: the ingress reaches only the edge, which forwards /realms// and /resources/ +alone. Service keycloak serves the Admin API in-cluster at http://keycloak:8080, Platform's KEYCLOAK_ADMIN_URL; the +admin console opens through `kubectl port-forward svc/keycloak 18080:8080` at http://127.0.0.1:18080, the URL +KC_HOSTNAME_ADMIN names, as on a Docker install. +*/}} +{{- $kc := .Values.keycloak.managed }} +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak +spec: + selector: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak") | nindent 4 }} + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak-edge + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak +spec: + selector: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak") | nindent 4 }} + ports: + - name: edge + port: 8081 + targetPort: edge +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak") | nindent 6 }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak") | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1000) | nindent 8 }} + containers: + - name: keycloak + image: {{ include "libredb-platform.image" $kc.image }} + imagePullPolicy: {{ $kc.image.pullPolicy }} + args: ["start"] + env: + - name: KC_DB + value: postgres + - name: KC_DB_URL + value: "jdbc:postgresql://{{ if include "libredb-platform.managedPostgres" . }}libredb-postgres:5432{{ else }}{{ .Values.postgresql.external.host }}:{{ .Values.postgresql.external.port }}{{ end }}/keycloak?sslmode=verify-full&sslrootcert=/etc/libredb/ca/ca.crt" + - name: KC_DB_USERNAME + value: libredb_keycloak + - name: KC_DB_POOL_MAX_SIZE + value: "20" + - name: KC_HOSTNAME + value: "https://{{ .Values.hosts.auth }}" + - name: KC_HOSTNAME_ADMIN + value: "http://127.0.0.1:18080" + - name: KC_PROXY_HEADERS + value: xforwarded + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_TRUSTSTORE_PATHS + value: /etc/libredb/ca/ca.crt + - name: LIQUIBASE_ANALYTICS_ENABLED + value: "false" + envFrom: + - secretRef: + name: libredb-keycloak-env + ports: + - name: http + containerPort: 8080 + - name: management + containerPort: 9000 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + startupProbe: + httpGet: + path: /health/started + port: management + periodSeconds: 5 + failureThreshold: 60 + readinessProbe: + httpGet: + path: /health/ready + port: management + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health/live + port: management + periodSeconds: 20 + resources: + {{- toYaml $kc.resources | nindent 12 }} + volumeMounts: + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + - name: edge + image: {{ include "libredb-platform.image" $kc.edge.image }} + imagePullPolicy: {{ $kc.edge.image.pullPolicy }} + args: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] + env: + - name: XDG_DATA_HOME + value: /tmp/caddy/data + - name: XDG_CONFIG_HOME + value: /tmp/caddy/config + ports: + - name: edge + containerPort: 8081 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: ["ALL"] + # The image's caddy binary carries the file capability cap_net_bind_service, and the kernel refuses to + # exec it when that capability is outside the bounding set; the edge itself listens on 8081. + add: ["NET_BIND_SERVICE"] + seccompProfile: + type: RuntimeDefault + readinessProbe: + tcpSocket: + port: edge + periodSeconds: 10 + resources: + {{- toYaml $kc.edge.resources | nindent 12 }} + volumeMounts: + - name: edge-config + mountPath: /etc/caddy + readOnly: true + - name: edge-tmp + mountPath: /tmp + volumes: + - name: ca + configMap: + name: libredb-ca + - name: edge-config + configMap: + name: libredb-keycloak-edge + - name: edge-tmp + emptyDir: {} + {{- with $kc.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $kc.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $kc.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.ingress.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: keycloak + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + tls: + - hosts: + - {{ .Values.hosts.auth | quote }} + secretName: {{ if eq .Values.tls.mode "generated" }}libredb-tls-auth{{ else }}{{ .Values.tls.secrets.auth }}{{ end }} + rules: + - host: {{ .Values.hosts.auth | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: keycloak-edge + port: + name: edge +{{- end }} +{{- end }} diff --git a/charts/libredb-platform/templates/networkpolicy.yaml b/charts/libredb-platform/templates/networkpolicy.yaml new file mode 100644 index 0000000..ba27039 --- /dev/null +++ b/charts/libredb-platform/templates/networkpolicy.yaml @@ -0,0 +1,52 @@ +{{- if .Values.networkPolicy.enabled }} +{{- /* +The installer's internal data network: the managed PostgreSQL server accepts connections only from this release's +pods, and Keycloak's port 8080 (the Admin API) only from them; the ingress reaches Keycloak through its edge alone. +*/}} +{{- if include "libredb-platform.managedPostgres" . }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: libredb-postgres + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "postgres") | nindent 6 }} + policyTypes: ["Ingress"] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - port: 5432 +--- +{{- end }} +{{- if include "libredb-platform.managedKeycloak" . }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: keycloak + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "keycloak") | nindent 6 }} + policyTypes: ["Ingress"] + ingress: + # Platform, Studio and the jobs reach the Admin API and the token endpoint in-cluster. + - from: + - podSelector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - port: 8080 + # Anyone may reach the edge, which forwards only the realm's public paths; restrict it to your ingress + # controller's namespace with an extra policy if you like. + - ports: + - port: 8081 +{{- end }} +{{- end }} diff --git a/charts/libredb-platform/templates/platform.yaml b/charts/libredb-platform/templates/platform.yaml new file mode 100644 index 0000000..305f3df --- /dev/null +++ b/charts/libredb-platform/templates/platform.yaml @@ -0,0 +1,172 @@ +{{- /* +Platform's `serve` (docs/deploy/platform-runtime.md), with the environment the bootstrap job rendered with the +installer's renderPlatformEnv. It never receives MIGRATE_DATABASE_URL: that lives only in the migrate job's Secret. +This release runs one replica. +*/}} +{{- $p := .Values.platform }} +{{- if and $p.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: libredb-platform-uploads + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: platform + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: + {{- toYaml $p.persistence.accessModes | nindent 4 }} + {{- with $p.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ $p.persistence.size }} +--- +{{- end }} +apiVersion: v1 +kind: Service +metadata: + name: libredb-platform + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: platform +spec: + selector: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "platform") | nindent 4 }} + ports: + - name: http + port: 3000 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: libredb-platform + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: platform +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "platform") | nindent 6 }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "platform") | nindent 8 }} + {{- with $p.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- /* The bootstrap job restarts Platform when its environment or CA bundle changed. */}} + {{- with $p.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + automountServiceAccountToken: false + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 1001) | nindent 8 }} + containers: + - name: platform + image: {{ include "libredb-platform.image" $p.image }} + imagePullPolicy: {{ $p.image.pullPolicy }} + args: ["serve"] + envFrom: + - secretRef: + name: libredb-platform-env + ports: + - name: http + containerPort: 3000 + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + startupProbe: + httpGet: + path: /api/health/live + port: http + periodSeconds: 5 + failureThreshold: 60 + livenessProbe: + httpGet: + path: /api/health/live + port: http + periodSeconds: 20 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /api/health/ready + port: http + periodSeconds: 10 + timeoutSeconds: 5 + resources: + {{- toYaml $p.resources | nindent 12 }} + volumeMounts: + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + - name: uploads + mountPath: /app/uploads + - name: tmp + mountPath: /tmp + - name: next-cache + mountPath: /app/.next/cache + volumes: + - name: ca + configMap: + name: libredb-ca + - name: uploads + {{- if $p.persistence.enabled }} + persistentVolumeClaim: + claimName: libredb-platform-uploads + {{- else }} + emptyDir: {} + {{- end }} + - name: tmp + emptyDir: {} + - name: next-cache + emptyDir: {} + {{- include "libredb-platform.platformScheduling" . | nindent 6 }} + {{- with $p.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.ingress.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: libredb-platform + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: platform + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + tls: + - hosts: + - {{ .Values.hosts.platform | quote }} + secretName: {{ if eq .Values.tls.mode "generated" }}libredb-tls-platform{{ else }}{{ .Values.tls.secrets.platform }}{{ end }} + rules: + - host: {{ .Values.hosts.platform | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: libredb-platform + port: + name: http +{{- end }} diff --git a/charts/libredb-platform/templates/postgres.yaml b/charts/libredb-platform/templates/postgres.yaml new file mode 100644 index 0000000..b9a9da3 --- /dev/null +++ b/charts/libredb-platform/templates/postgres.yaml @@ -0,0 +1,223 @@ +{{- if include "libredb-platform.managedPostgres" . }} +{{- /* +The managed PostgreSQL server, as the installer's compose service: TLS with the internal CA's leaf, a pg_hba.conf that +accepts only hostssl with SCRAM, and the superuser password from the bootstrap job. The provisioning job then runs the +installer's provisioning script (installer/src/sql/provision.ts), which sends SCRAM verifiers only. +*/}} +{{- $pg := .Values.postgresql.managed }} +apiVersion: v1 +kind: Service +metadata: + name: libredb-postgres + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + selector: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "postgres") | nindent 4 }} + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: libredb-postgres + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + serviceName: libredb-postgres + replicas: 1 + selector: + matchLabels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "postgres") | nindent 6 }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "postgres") | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 999) | nindent 8 }} + containers: + - name: postgres + image: {{ include "libredb-platform.image" $pg.image }} + imagePullPolicy: {{ $pg.image.pullPolicy }} + args: + - postgres + - -c + - ssl=on + - -c + - ssl_cert_file=/etc/libredb/tls/server.crt + - -c + - ssl_key_file=/etc/libredb/tls/server.key + - -c + - hba_file=/etc/libredb/postgres/pg_hba.conf + - -c + - password_encryption=scram-sha-256 + env: + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: libredb-postgres-superuser + key: POSTGRES_PASSWORD + - name: POSTGRES_INITDB_ARGS + value: --auth-host=scram-sha-256 --auth-local=scram-sha-256 + ports: + - name: postgres + containerPort: 5432 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + readinessProbe: + exec: + command: ["pg_isready", "-h", "127.0.0.1", "-U", "postgres", "-d", "postgres"] + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: ["pg_isready", "-h", "127.0.0.1", "-U", "postgres", "-d", "postgres"] + initialDelaySeconds: 60 + periodSeconds: 20 + timeoutSeconds: 5 + resources: + {{- toYaml $pg.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql + - name: tls + mountPath: /etc/libredb/tls + readOnly: true + - name: config + mountPath: /etc/libredb/postgres + readOnly: true + - name: run + mountPath: /var/run/postgresql + volumes: + - name: tls + secret: + secretName: libredb-postgres-tls + # PostgreSQL accepts a key owned by root only with mode 0640 or stricter. + defaultMode: 0640 + - name: config + configMap: + name: libredb-postgres-config + - name: run + emptyDir: {} + {{- with $pg.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $pg.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $pg.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + volumeClaimTemplates: + # A StatefulSet's volume claims outlive the StatefulSet, and helm uninstall, by Kubernetes' own rule. + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + {{- with $pg.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ $pg.persistence.size }} +--- +{{- $provision := dict "revision" .Release.Revision "image" (include "libredb-platform.image" $pg.image) "installation" (include "libredb-platform.installation" .) }} +apiVersion: batch/v1 +kind: Job +metadata: + name: libredb-postgres-provision-{{ include "libredb-platform.hash" (toJson $provision) }} + labels: + {{- include "libredb-platform.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres-provision + annotations: + # Argo CD renders with helm template, where every revision is 1: as a Sync hook replaced on each sync, the job + # runs again on every sync as on every helm upgrade. Helm ignores these annotations. + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + # The job retries until what it needs is up, so it gets more attempts than the bootstrap hook. + backoffLimit: {{ add .Values.jobs.backoffLimit 4 }} + activeDeadlineSeconds: {{ .Values.jobs.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "libredb-platform.selectorLabels" (dict "root" . "component" "postgres-provision") | nindent 8 }} + spec: + automountServiceAccountToken: false + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- include "libredb-platform.podSecurity" (dict "root" $ "uid" 999) | nindent 8 }} + initContainers: + {{- include "libredb-platform.waitForPostgres" . | nindent 8 }} + containers: + - name: provision + image: {{ include "libredb-platform.image" $pg.image }} + imagePullPolicy: {{ $pg.image.pullPolicy }} + # The installer's provisioning script as the superuser, over verified TLS; it converges an existing server. + # A renewed leaf restarts the server instead: the bootstrap job restarts it when its leaf Secret changed. + command: ["psql", "-X", "-v", "ON_ERROR_STOP=1", "-f", "/etc/libredb/provision/provision.sql"] + env: + - name: PGHOST + value: libredb-postgres + - name: PGPORT + value: "5432" + - name: PGUSER + value: postgres + - name: PGDATABASE + value: postgres + - name: PGSSLMODE + value: verify-full + - name: PGSSLROOTCERT + value: /etc/libredb/ca/ca.crt + - name: PGCONNECT_TIMEOUT + value: "10" + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: libredb-postgres-superuser + key: POSTGRES_PASSWORD + securityContext: + {{- include "libredb-platform.containerSecurity" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + volumeMounts: + - name: provision + mountPath: /etc/libredb/provision + readOnly: true + - name: ca + mountPath: /etc/libredb/ca + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: provision + secret: + secretName: libredb-postgres-provision + - name: ca + configMap: + name: libredb-ca + - name: tmp + emptyDir: {} +{{- end }} diff --git a/charts/libredb-platform/tests/e2e/coredns-rewrite.sh b/charts/libredb-platform/tests/e2e/coredns-rewrite.sh new file mode 100755 index 0000000..4c8b5d5 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/coredns-rewrite.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Makes every *.libredb.test name resolve, inside the kind cluster, to the Traefik Service, the way a real site's DNS +# points the three hosts at its ingress: Platform and Studio call Keycloak's public issuer URL from their pods. +set -euo pipefail +corefile=$(kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}') +if grep -q 'libredb\\.test' <<<"$corefile"; then + echo "CoreDNS already rewrites *.libredb.test" + exit 0 +fi +rewritten=$(sed 's|^\( ready\)$|\1\n rewrite name regex (.*)\\.libredb\\.test traefik.traefik.svc.cluster.local answer auto|' <<<"$corefile") +kubectl -n kube-system create configmap coredns --from-literal=Corefile="$rewritten" --dry-run=client -o yaml | kubectl -n kube-system apply -f - >/dev/null +kubectl -n kube-system rollout restart deployment coredns >/dev/null +kubectl -n kube-system rollout status deployment coredns --timeout=120s diff --git a/charts/libredb-platform/tests/e2e/kind-config.yaml b/charts/libredb-platform/tests/e2e/kind-config.yaml new file mode 100644 index 0000000..63f1bee --- /dev/null +++ b/charts/libredb-platform/tests/e2e/kind-config.yaml @@ -0,0 +1,17 @@ +# The kind cluster of the end-to-end test: Traefik's node ports reach the host on 127.0.0.1:80 and 127.0.0.1:443. +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: libredb-helm +nodes: + - role: control-plane + labels: + ingress-ready: "true" + extraPortMappings: + - containerPort: 30443 + hostPort: 443 + listenAddress: "127.0.0.1" + protocol: TCP + - containerPort: 30080 + hostPort: 80 + listenAddress: "127.0.0.1" + protocol: TCP diff --git a/charts/libredb-platform/tests/e2e/run-argocd.sh b/charts/libredb-platform/tests/e2e/run-argocd.sh new file mode 100755 index 0000000..6329ac4 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-argocd.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# The Argo CD test of the chart, on the kind cluster run-e2e.sh set up (Traefik, the *.libredb.test DNS), with no Helm +# release of the chart in the cluster, since it uses the same hosts: +# +# LIBREDB_E2E_LICENSE=kind.license LIBREDB_E2E_CHART_REPO=/dir/with/index.yaml/and/tgz \ +# charts/libredb-platform/tests/e2e/run-argocd.sh +# +# It installs Argo CD with label resource tracking, the method under which a tool would prune the objects the jobs +# write if they carried the release's instance label, serves the packaged chart from a Helm repository in the cluster, +# and syncs an Application with automated prune and self-heal into a namespace that enforces the restricted Pod +# Security Standard. It checks that the app turns Synced and Healthy, the endpoints answer, the jobs' Secrets survive +# the prune of every sync, a new sync runs the jobs again without touching the seal keys, and a changed value reaches +# the running Platform through the PostSync restart job. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +license=${LIBREDB_E2E_LICENSE:?a license for platform.libredb.test} +repo=${LIBREDB_E2E_CHART_REPO:?a directory with the packaged chart and its index.yaml} +argocd_version=${LIBREDB_E2E_ARGOCD_VERSION:-v3.5.5} +namespace=libredb-argo +# The chart's own version, at the indent of an index entry, not a dependency's. +version=$(awk '/^ version:/{print $2; exit}' "$repo/index.yaml" | tr -d '"') + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +app() { kubectl -n argocd get application libredb -o "jsonpath=$1"; } +ready() { curl -s --resolve "$1:443:127.0.0.1" -k -o /dev/null -w '%{http_code}' "https://$1$2"; } +hash_of() { kubectl -n "$namespace" get secret "$1" -o "jsonpath={.data.$2}" | sha256sum | cut -d' ' -f1; } +wait_synced() { + for _ in $(seq 1 180); do + if [ "$(app '{.status.sync.status}')" = "Synced" ] && [ "$(app '{.status.health.status}')" = "Healthy" ] \ + && [ "$(app '{.status.operationState.phase}')" = "Succeeded" ]; then return; fi + sleep 5 + done + fail "the app is $(app '{.status.sync.status}')/$(app '{.status.health.status}'), operation $(app '{.status.operationState.phase}'): $(app '{.status.operationState.message}')" +} + +step "Argo CD $argocd_version with label resource tracking" +kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f - >/dev/null +# LIBREDB_E2E_ARGOCD_MANIFEST names a downloaded install.yaml, for a cluster whose nodes reach no registry or GitHub. +kubectl apply -n argocd --server-side --force-conflicts \ + -f "${LIBREDB_E2E_ARGOCD_MANIFEST:-https://raw.githubusercontent.com/argoproj/argo-cd/$argocd_version/manifests/install.yaml}" >/dev/null +kubectl -n argocd patch configmap argocd-cm --type merge -p '{"data":{"application.resourceTrackingMethod":"label"}}' >/dev/null +kubectl -n argocd rollout restart statefulset/argocd-application-controller >/dev/null +for deploy in argocd-repo-server argocd-server; do kubectl -n argocd rollout status "deploy/$deploy" --timeout=300s >/dev/null; done +kubectl -n argocd rollout status statefulset/argocd-application-controller --timeout=300s >/dev/null + +step "a Helm repository in the cluster serving libredb-platform $version" +kubectl create namespace chartrepo --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl -n chartrepo create configmap charts --from-file="$repo" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts -f - >/dev/null +kubectl -n chartrepo apply -f - >/dev/null <<'EOF' +apiVersion: apps/v1 +kind: Deployment +metadata: {name: chartrepo} +spec: + selector: {matchLabels: {app: chartrepo}} + template: + metadata: {labels: {app: chartrepo}} + spec: + containers: + - name: httpd + image: busybox:1.37 + command: ["httpd", "-f", "-p", "8080", "-h", "/srv"] + volumeMounts: [{name: charts, mountPath: /srv}] + volumes: [{name: charts, configMap: {name: charts}}] +--- +apiVersion: v1 +kind: Service +metadata: {name: chartrepo} +spec: {selector: {app: chartrepo}, ports: [{port: 8080}]} +EOF +kubectl -n chartrepo rollout restart deploy/chartrepo >/dev/null +kubectl -n chartrepo rollout status deploy/chartrepo --timeout=120s >/dev/null + +step "the Application: automated sync with prune and self-heal into $namespace" +kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl label namespace "$namespace" pod-security.kubernetes.io/enforce=restricted --overwrite >/dev/null +kubectl -n "$namespace" create secret generic libredb-license --from-file=license="$license" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +application() { + cat </dev/null +started=$(date +%s) +wait_synced +echo "Synced and Healthy in $(( $(date +%s) - started )) s" + +step "endpoints, license and the jobs' Secrets" +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready" +[ "$(ready studio.libredb.test /api/ready)" = "200" ] || fail "Studio is not ready" +[ "$(ready auth.libredb.test /realms/libredb/.well-known/openid-configuration)" = "200" ] || fail "Keycloak's realm does not answer" +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +seal=$(hash_of libredb-secrets 'seal-keys\.json') +annotations=$(kubectl -n "$namespace" get secret libredb-secrets -o 'jsonpath={.metadata.annotations}') +grep -q 'Prune=false' <<<"$annotations" || fail "libredb-secrets lacks Argo CD's Prune=false: $annotations" +labels=$(kubectl -n "$namespace" get secret libredb-secrets -o 'jsonpath={.metadata.labels}') +grep -q 'app.kubernetes.io/instance' <<<"$labels" && fail "libredb-secrets carries the tracking label: $labels" +echo "Platform, Studio and Keycloak answer, license active, the jobs' Secrets carry no tracking label" + +step "a new sync with prune runs the jobs again and keeps every Secret of the jobs" +migrate=$(kubectl -n "$namespace" get jobs -l app.kubernetes.io/component=platform-migrate -o 'jsonpath={.items[0].metadata.uid}') +kubectl -n argocd patch application libredb --type merge \ + -p "{\"operation\":{\"initiatedBy\":{\"username\":\"e2e\"},\"sync\":{\"revision\":\"$version\",\"prune\":true}}}" >/dev/null +sleep 5 +wait_synced +again=$(kubectl -n "$namespace" get jobs -l app.kubernetes.io/component=platform-migrate -o 'jsonpath={.items[0].metadata.uid}') +[ -n "$again" ] && [ "$again" != "$migrate" ] || fail "the sync did not run the migrate job again" +for secret in libredb-secrets libredb-tls libredb-installer-state libredb-platform-env libredb-studio-env; do + kubectl -n "$namespace" get secret "$secret" >/dev/null || fail "the sync pruned Secret $secret" +done +[ "$(hash_of libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the sync changed the seal keys" +echo "the migrate job ran again, every Secret of the jobs is there, the seal keys unchanged" + +step "a changed value reaches the running Platform through the PostSync restart job" +application "queryLogRetentionDays: 30" | kubectl apply -f - >/dev/null +for _ in $(seq 1 60); do + [ "$(kubectl -n "$namespace" get secret libredb-platform-env -o 'jsonpath={.data.QUERY_LOG_RETENTION_DAYS}' | base64 -d)" = "30" ] && break + sleep 5 +done +wait_synced +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=300s >/dev/null +running=$(kubectl -n "$namespace" exec deploy/libredb-platform -- printenv QUERY_LOG_RETENTION_DAYS) +[ "$running" = "30" ] || fail "the running Platform has QUERY_LOG_RETENTION_DAYS=$running" +[ "$(hash_of libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the change changed the seal keys" +echo "the running Platform has QUERY_LOG_RETENTION_DAYS=30, the seal keys unchanged" + +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-e2e.sh b/charts/libredb-platform/tests/e2e/run-e2e.sh new file mode 100755 index 0000000..9813b5b --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-e2e.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# The end-to-end test of the Helm chart on a local kind cluster, which it creates when missing: +# +# LIBREDB_E2E_LICENSE=kind.license charts/libredb-platform/tests/e2e/run-e2e.sh +# +# The license must name the host platform.libredb.test. The test installs the chart with a managed PostgreSQL and +# Keycloak, checks the endpoints and the edge's path rules against the internal CA, the license, Studio's heartbeat +# and its seal canary, runs the browser suite (installer/e2e/kubernetes), upgrades with a changed value and checks that +# the seal keys and the CA stay, and finally uninstalls and installs again and checks the same. It uses only the kind +# cluster it names; it never touches other containers or volumes of the host's Docker. +# Needs kind, kubectl, helm, curl and bun on PATH. LIBREDB_E2E_KEEP=1 keeps the installation running at the end. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +cluster=${LIBREDB_E2E_CLUSTER:-libredb-helm} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +install_chart() { + helm upgrade --install libredb "$chart" -n "$namespace" --create-namespace -f "$here/values-kind.yaml" \ + --set-file platform.license.value="$license" "$@" --wait --wait-for-jobs --timeout 10m +} + +secret_hash() { kubectl -n "$namespace" get secret "$1" -o "jsonpath={.data.$2}" | sha256sum | cut -d' ' -f1; } + +if [ "${LIBREDB_E2E_EXISTING_CLUSTER:-0}" = "1" ]; then + # A cluster this script set up before, reached through KUBECONFIG, such as from a container on kind's network. + step "the cluster of KUBECONFIG, set up earlier" +else + step "kind cluster $cluster" + if ! kind get clusters | grep -qx "$cluster"; then + sed "s/^name: .*/name: $cluster/" "$here/kind-config.yaml" > "$work/kind-config.yaml" + kind create cluster --config "$work/kind-config.yaml" --wait 180s + fi + kubectl config use-context "kind-$cluster" >/dev/null + + step "Traefik and the in-cluster DNS of *.libredb.test" + helm repo add traefik https://traefik.github.io/charts >/dev/null 2>&1 || true + helm repo update traefik >/dev/null + helm upgrade --install traefik traefik/traefik --version 41.7.1 -n traefik --create-namespace \ + -f "$here/traefik-values.yaml" --wait --timeout 5m >/dev/null + "$here/coredns-rewrite.sh" +fi + +step "chart dependencies" +[ -f "$chart/charts/libredb-studio-0.1.100.tgz" ] || helm dependency build "$chart" + +step "install into a namespace that enforces the restricted Pod Security Standard" +kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl label namespace "$namespace" pod-security.kubernetes.io/enforce=restricted --overwrite >/dev/null +started=$(date +%s) +install_chart >/dev/null +echo "installed in $(( $(date +%s) - started )) s" +errors=$(kubectl -n "$namespace" get pods --no-headers | awk '$3 == "Error" || $3 == "CrashLoopBackOff"' | wc -l) +[ "$errors" -eq 0 ] || fail "$errors pods failed during the install" + +step "endpoints, the edge's path rules and TLS against the internal CA" +kubectl -n "$namespace" get secret libredb-tls-platform -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" +resolve=(--resolve platform.libredb.test:443:127.0.0.1 --resolve studio.libredb.test:443:127.0.0.1 --resolve auth.libredb.test:443:127.0.0.1) +expect_status() { + local status + status=$(curl -s "${resolve[@]}" --cacert "$work/ca.crt" --path-as-is -o /dev/null -w '%{http_code}' "$2") + [ "$status" = "$1" ] || fail "$2 answered $status, not $1" + printf '%s %s\n' "$status" "$2" +} +expect_status 200 https://platform.libredb.test/api/health/ready +expect_status 200 https://studio.libredb.test/api/ready +expect_status 200 https://auth.libredb.test/realms/libredb/.well-known/openid-configuration +expect_status 404 https://auth.libredb.test/admin/ +expect_status 404 https://auth.libredb.test/realms/master/ +expect_status 400 "https://auth.libredb.test/realms/libredb/..;/master/" + +step "network policies: a pod of another namespace reaches Platform but not Keycloak's Admin API or PostgreSQL" +probe() { + kubectl -n default run "np-probe-$RANDOM" --rm -i --restart=Never --quiet --image=busybox:1.37 -- \ + sh -c "nc -z -w 3 $1 $2 && echo open || echo closed" 2>/dev/null | tail -1 +} +[ "$(probe "libredb-platform.$namespace.svc" 3000)" = "open" ] || fail "the probe pod cannot reach Platform at all" +[ "$(probe "keycloak.$namespace.svc" 8080)" = "closed" ] || fail "a pod outside the release reaches Keycloak's Admin API" +[ "$(probe "libredb-postgres.$namespace.svc" 5432)" = "closed" ] || fail "a pod outside the release reaches PostgreSQL" +echo "Keycloak's Admin API and PostgreSQL refuse a pod outside the release" + +step "license and Studio's heartbeat" +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +# Platform publishes the canary when it starts, and Studio reports it with its next heartbeat, every 30 seconds. +for _ in $(seq 1 30); do + heartbeat=$(kubectl -n "$namespace" exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -h 127.0.0.1 -U postgres -d libredb -c "SELECT canary FROM bridge.studio_instances_v1"') + grep -q '"ok"' <<<"$heartbeat" && break + sleep 5 +done +grep -q '"ok"' <<<"$heartbeat" || fail "Studio's seal canary is not ok: $heartbeat" +echo "license active; seal canary $heartbeat" + +step "browser suite" +account="$work/first-admin.json" +first_admin_file="${LIBREDB_E2E_ACCOUNT_FILE:-$account}" +# The license's id, from its payload, the third dot-separated field in base64url. +license_id=$(cut -d. -f3 "$license" | tr '_-' '/+' | base64 -d 2>/dev/null | grep -o '"id":"[^"]*"' | cut -d'"' -f4 || true) +(cd "$installer" && LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_ACCOUNT_FILE="$first_admin_file" LIBREDB_E2E_LICENSE_ID="$license_id" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts) + +step "upgrade with a changed value keeps the seal keys and the CA" +seal=$(secret_hash libredb-secrets 'seal-keys\.json') +ca=$(secret_hash libredb-tls 'ca\.crt') +install_chart --set platform.queryLogRetentionDays=30 >/dev/null +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the upgrade changed the seal keys" +[ "$(secret_hash libredb-tls 'ca\.crt')" = "$ca" ] || fail "the upgrade changed the internal CA" +retention=$(kubectl -n "$namespace" get secret libredb-platform-env -o 'jsonpath={.data.QUERY_LOG_RETENTION_DAYS}' | base64 -d) +[ "$retention" = "30" ] || fail "QUERY_LOG_RETENTION_DAYS is $retention after the upgrade" +# The bootstrap hook restarted Platform, whose environment changed, so the running server has the new value. +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=180s >/dev/null +running=$(kubectl -n "$namespace" exec deploy/libredb-platform -- printenv QUERY_LOG_RETENTION_DAYS) +[ "$running" = "30" ] || fail "the running Platform has QUERY_LOG_RETENTION_DAYS=$running after the upgrade" +expect_status 200 https://platform.libredb.test/api/health/ready +echo "seal keys and CA kept, QUERY_LOG_RETENTION_DAYS=30 in the Secret and the running server" + +step "a rollback within the release runs the bootstrap with the earlier values and restarts Platform" +helm -n "$namespace" rollback libredb --wait --timeout 10m >/dev/null +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=180s >/dev/null +running=$(kubectl -n "$namespace" exec deploy/libredb-platform -- printenv QUERY_LOG_RETENTION_DAYS) +[ "$running" = "90" ] || fail "the running Platform has QUERY_LOG_RETENTION_DAYS=$running after the rollback" +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the rollback changed the seal keys" +expect_status 200 https://platform.libredb.test/api/health/ready +echo "rolled back to QUERY_LOG_RETENTION_DAYS=90, seal keys kept" + +step "uninstall keeps the generated Secrets and the volumes; a new install reuses them" +helm -n "$namespace" uninstall libredb --wait >/dev/null +for name in libredb-secrets libredb-tls libredb-installer-state; do + kubectl -n "$namespace" get secret "$name" >/dev/null || fail "uninstall removed Secret $name" +done +kubectl -n "$namespace" get pvc data-libredb-postgres-0 >/dev/null || fail "uninstall removed the database volume" +install_chart >/dev/null +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the reinstall changed the seal keys" +[ "$(secret_hash libredb-tls 'ca\.crt')" = "$ca" ] || fail "the reinstall changed the internal CA" +kubectl -n "$namespace" logs -l app.kubernetes.io/component=keycloak-configure --tail=20 | grep -c "realm already converged" >/dev/null \ + || fail "the reinstall's keycloak job did not find the realm converged" +expect_status 200 https://platform.libredb.test/api/health/ready +expect_status 200 https://studio.libredb.test/api/ready +echo "reinstall reused the seal keys, the CA, the database and the realm" + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-external.sh b/charts/libredb-platform/tests/e2e/run-external.sh new file mode 100755 index 0000000..00908cd --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-external.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# The end-to-end test of the chart against a customer's own PostgreSQL and Keycloak, on the kind cluster that +# run-e2e.sh set up (Traefik, the *.libredb.test DNS): +# +# LIBREDB_E2E_LICENSE=kind-ext.license charts/libredb-platform/tests/e2e/run-external.sh +# +# The license must name platform-ext.libredb.test. Namespace corp plays the customer: a PostgreSQL server with TLS +# from a CA of its own, at corp-pg.corp.svc.cluster.local, and a Keycloak at https://sso-ext.libredb.test behind +# Traefik with a certificate of the same CA and an admin user in realm master. The chart installs into libredb-ext +# with postgresql.mode external (a DBA runs the script the chart writes) and keycloak.mode external with provisioning +# installer, its own TLS Secrets for the ingress (tls.mode secrets) and the license in a Secret of its own +# (platform.license.existingSecret). The test checks that the install converges, the realm holds the LibreDB objects, +# Platform's client can list users, the license is active, Studio's seal canary is ok, and the ingress serves the +# customer's certificates. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform-ext.libredb.test} +namespace=libredb-ext +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +step "a fresh start: namespaces corp and $namespace" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace corp "$namespace" --ignore-not-found --wait=true >/dev/null + +step "the customer's CA and certificates" +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 -subj "/CN=Corp test CA" \ + -keyout "$work/ca.key" -out "$work/ca.crt" 2>/dev/null +leaf() { + openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=$2" -keyout "$work/$1.key" -out "$work/$1.csr" 2>/dev/null + openssl x509 -req -in "$work/$1.csr" -CA "$work/ca.crt" -CAkey "$work/ca.key" -CAcreateserial -days 30 \ + -extfile <(printf 'subjectAltName=DNS:%s\nextendedKeyUsage=serverAuth\n' "$2") -out "$work/$1.crt" 2>/dev/null +} +leaf pg corp-pg.corp.svc.cluster.local +leaf sso sso-ext.libredb.test +leaf platform platform-ext.libredb.test +leaf studio studio-ext.libredb.test + +step "the customer's PostgreSQL and Keycloak in namespace corp" +kubectl create namespace corp --dry-run=client -o yaml | kubectl apply -f - >/dev/null +superuser=$(openssl rand -hex 16) +admin_password=$(openssl rand -hex 16) +kubectl -n corp create secret generic corp-pg-tls --from-file=server.crt="$work/pg.crt" --from-file=server.key="$work/pg.key" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl -n corp create secret tls sso-ext-tls --cert="$work/sso.crt" --key="$work/sso.key" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +postgres_image=$(helm show values "$chart" | awk '/^postgresql:/{p=1} p && /repository:/{r=$2} p && /tag:/{gsub(/"/,"",$2); print r":"$2; exit}') +keycloak_image=$(helm show values "$chart" | awk '/^keycloak:/{p=1} p && /repository: quay/{r=$2} p && r && /tag:/{gsub(/"/,"",$2); print r":"$2; exit}') +kubectl -n corp apply -f - >/dev/null </dev/null +kubectl -n corp rollout status deploy/corp-sso --timeout=300s >/dev/null +resolve=(--resolve sso-ext.libredb.test:443:127.0.0.1 --resolve platform-ext.libredb.test:443:127.0.0.1 --resolve studio-ext.libredb.test:443:127.0.0.1) +# Traefik serves the new Ingress and its certificate a few seconds after it exists. +token="" +for _ in $(seq 1 30); do + token=$(curl -sf "${resolve[@]}" --cacert "$work/ca.crt" https://sso-ext.libredb.test/realms/master/protocol/openid-connect/token \ + -d grant_type=password -d client_id=admin-cli -d username=admin --data-urlencode "password=$admin_password" \ + | sed 's/.*"access_token":"\([^"]*\)".*/\1/') && [ -n "$token" ] && break + sleep 2 +done +[ -n "$token" ] || fail "the customer's Keycloak gave its admin no token" +# The customer's realm exists before the install: provisioning installer configures objects in it, never the realm. +curl -sf "${resolve[@]}" --cacert "$work/ca.crt" -X POST https://sso-ext.libredb.test/admin/realms \ + -H "authorization: Bearer $token" -H 'content-type: application/json' -d '{"realm":"corp","enabled":true}' || true + +step "install into $namespace against them" +kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl label namespace "$namespace" pod-security.kubernetes.io/enforce=restricted --overwrite >/dev/null +kubectl -n "$namespace" create secret generic corp-keycloak-admin --from-literal=admin-password="$admin_password" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl -n "$namespace" create secret generic corp-license --from-file=token="$license" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl -n "$namespace" create secret tls corp-platform-tls --cert="$work/platform.crt" --key="$work/platform.key" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +kubectl -n "$namespace" create secret tls corp-studio-tls --cert="$work/studio.crt" --key="$work/studio.key" --dry-run=client -o yaml | kubectl apply -f - >/dev/null +cat > "$work/values.yaml" </dev/null + +step "the DBA runs the provisioning script the chart wrote" +for _ in $(seq 1 60); do kubectl -n "$namespace" get secret libredb-postgres-dba-script >/dev/null 2>&1 && break; sleep 2; done +kubectl -n "$namespace" get secret libredb-postgres-dba-script -o 'jsonpath={.data.provision\.sql}' | base64 -d > "$work/provision.sql" +grep -q 'SCRAM-SHA-256\$' "$work/provision.sql" || fail "the DBA script holds no SCRAM verifier" +kubectl -n corp exec -i corp-pg -- sh -c "PGPASSWORD='$superuser' psql -X -q -v ON_ERROR_STOP=1 -h 127.0.0.1 -U postgres -d postgres -f -" < "$work/provision.sql" >/dev/null + +step "the installation converges" +for _ in $(seq 1 120); do + ready=$(kubectl -n "$namespace" get deploy libredb-platform libredb-studio -o 'jsonpath={.items[*].status.readyReplicas}') + [ "$ready" = "1 1" ] && break + sleep 5 +done +[ "$ready" = "1 1" ] || fail "Platform and Studio did not turn ready: $ready" +kubectl -n "$namespace" wait --for=condition=complete job -l app.kubernetes.io/component=keycloak-configure --timeout=300s >/dev/null +kubectl -n "$namespace" wait --for=condition=complete job -l app.kubernetes.io/component=platform-migrate --timeout=300s >/dev/null +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +kubectl -n "$namespace" get statefulset libredb-postgres >/dev/null 2>&1 && fail "an external PostgreSQL install runs a managed server" +kubectl -n "$namespace" get deploy keycloak >/dev/null 2>&1 && fail "an external Keycloak install runs a managed Keycloak" + +step "Platform's client signs in to the customer's realm and lists users through its service account" +platform_secret=$(kubectl -n "$namespace" get secret libredb-keycloak-config -o 'jsonpath={.data.keycloak-client-libredb-platform}' | base64 -d) +client_token=$(curl -sf "${resolve[@]}" --cacert "$work/ca.crt" https://sso-ext.libredb.test/realms/corp/protocol/openid-connect/token \ + -d grant_type=client_credentials -d client_id=libredb-platform --data-urlencode "client_secret=$platform_secret" | sed 's/.*"access_token":"\([^"]*\)".*/\1/') +status=$(curl -s "${resolve[@]}" --cacert "$work/ca.crt" -o /dev/null -w '%{http_code}' -H "authorization: Bearer $client_token" \ + https://sso-ext.libredb.test/admin/realms/corp/users?max=1) +[ "$status" = "200" ] || fail "libredb-platform's service account cannot list users: $status" +roles=$(curl -sf "${resolve[@]}" --cacert "$work/ca.crt" -H "authorization: Bearer $token" "https://sso-ext.libredb.test/admin/realms/corp/clients?clientId=libredb") +grep -q '"clientId":"libredb"' <<<"$roles" || fail "the realm holds no client libredb" + +step "Studio's seal canary" +for _ in $(seq 1 30); do + canary=$(kubectl -n corp exec corp-pg -- sh -c "PGPASSWORD='$superuser' psql -X -A -t -h 127.0.0.1 -U postgres -d libredb -c 'SELECT canary FROM bridge.studio_instances_v1'") + grep -q '"ok"' <<<"$canary" && break + sleep 5 +done +grep -q '"ok"' <<<"$canary" || fail "Studio's seal canary is not ok: $canary" +step "the ingress serves the customer's certificates" +for url in https://platform-ext.libredb.test/api/health/ready https://studio-ext.libredb.test/api/ready; do + status=$(curl -s "${resolve[@]}" --cacert "$work/ca.crt" -o /dev/null -w '%{http_code}' "$url") + [ "$status" = "200" ] || fail "$url answered $status over the customer's certificate" + echo "200 $url" +done +if kubectl -n "$namespace" get secret libredb-tls-platform >/dev/null 2>&1; then fail "tls.mode secrets wrote an ingress Secret of the internal CA"; fi + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" corp --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-lifecycle.sh b/charts/libredb-platform/tests/e2e/run-lifecycle.sh new file mode 100755 index 0000000..b1770c8 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-lifecycle.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# The lifecycle test of a Helm install, on the installation run-e2e.sh left running (LIBREDB_E2E_KEEP=1): +# +# LIBREDB_E2E_LICENSE=active.license LIBREDB_E2E_LICENSE_EXPIRING=expiring.license \ +# LIBREDB_E2E_LICENSE_EXPIRED=expired.license charts/libredb-platform/tests/e2e/run-lifecycle.sh +# +# Certificate renewal: the clock of a kind node cannot move, so the test puts the installation where it would stand +# after two years: it replaces the PostgreSQL leaf and Platform's ingress leaf with leaves of the same internal CA that +# expire in 10 days, and restarts PostgreSQL so it serves its short leaf. An upgrade with unchanged values must then +# re-issue both, PostgreSQL must serve its new leaf after the restart job, the ingress its new one, and the apps keep +# working over verify-full. License lifecycle: an upgrade with a license that expires within 30 days, one that has +# expired and then the active one again must each reach the running Platform, which logs the state. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +active=${LIBREDB_E2E_LICENSE:?the active license for platform.libredb.test} +expiring=${LIBREDB_E2E_LICENSE_EXPIRING:?a license for platform.libredb.test that expires within 30 days} +expired=${LIBREDB_E2E_LICENSE_EXPIRED:?a license for platform.libredb.test past its grace period} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +# Days until a certificate's notAfter; a read that failed gives none, so a wait never takes it for an answer. +days_left() { + [ -n "$1" ] || { echo -1; return; } + echo $(( ( $(date -d "$1" +%s) - $(date +%s) ) / 86400 )) +} + +upgrade() { + local license=$1 + shift + helm upgrade libredb "$chart" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" "$@" \ + --wait --wait-for-jobs --timeout 10m >/dev/null +} + +# The leaf PostgreSQL serves, read after the SSLRequest of the protocol from Platform's pod, which the network +# policies let in. +pg_not_after() { + kubectl -n "$namespace" exec deploy/libredb-platform -- node -e ' + const net = require("net"), tls = require("tls"); + const socket = net.connect(5432, "libredb-postgres", () => { const b = Buffer.alloc(8); b.writeInt32BE(8); b.writeInt32BE(80877103, 4); socket.write(b); }); + socket.once("data", (reply) => { + if (String(reply) !== "S") { console.error("no TLS"); process.exit(1); } + const secure = tls.connect({ socket, servername: "libredb-postgres", rejectUnauthorized: false }, () => { console.log(secure.getPeerCertificate().valid_to); process.exit(0); }); + });' +} + +ingress_not_after() { + openssl s_client -connect 127.0.0.1:443 -servername "$1" /dev/null | openssl x509 -noout -enddate | cut -d= -f2 +} + +platform_license_line() { + kubectl -n "$namespace" logs deploy/libredb-platform | grep '"message":"License' | tail -1 +} + +wait_platform() { + kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=300s >/dev/null + for _ in $(seq 1 60); do + [ "$(curl -s --resolve platform.libredb.test:443:127.0.0.1 -k -o /dev/null -w '%{http_code}' https://platform.libredb.test/api/health/ready)" = "200" ] && return + sleep 3 + done + fail "Platform did not turn ready" +} + +step "leaves of the internal CA that expire in 10 days, as after two years" +kubectl -n "$namespace" get secret libredb-tls -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" +kubectl -n "$namespace" get secret libredb-tls -o 'jsonpath={.data.ca\.key}' | base64 -d > "$work/ca.key" +short_leaf() { + local name=$1 sans=$2 + openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=$name" -keyout "$work/$name.key" -out "$work/$name.csr" 2>/dev/null + openssl x509 -req -in "$work/$name.csr" -CA "$work/ca.crt" -CAkey "$work/ca.key" -CAcreateserial -days 10 \ + -extfile <(printf 'subjectAltName=%s\nextendedKeyUsage=serverAuth\nbasicConstraints=CA:FALSE\n' "$sans") -out "$work/$name.crt" 2>/dev/null +} +b64() { base64 -w0 < "$1"; } +short_leaf postgres "DNS:libredb-postgres,DNS:libredb-postgres.$namespace,DNS:libredb-postgres.$namespace.svc,DNS:libredb-postgres.$namespace.svc.cluster.local" +short_leaf platform "DNS:platform.libredb.test" +kubectl -n "$namespace" patch secret libredb-tls --type merge -p "{\"data\":{\"postgres.crt\":\"$(b64 "$work/postgres.crt")\",\"postgres.key\":\"$(b64 "$work/postgres.key")\",\"platform.crt\":\"$(b64 "$work/platform.crt")\",\"platform.key\":\"$(b64 "$work/platform.key")\"}}" >/dev/null +kubectl -n "$namespace" patch secret libredb-postgres-tls --type merge -p "{\"data\":{\"server.crt\":\"$(b64 "$work/postgres.crt")\",\"server.key\":\"$(b64 "$work/postgres.key")\"}}" >/dev/null +kubectl -n "$namespace" patch secret libredb-tls-platform --type merge -p "{\"data\":{\"tls.crt\":\"$(b64 "$work/platform.crt")\",\"tls.key\":\"$(b64 "$work/platform.key")\"}}" >/dev/null +kubectl -n "$namespace" rollout restart statefulset/libredb-postgres >/dev/null +kubectl -n "$namespace" rollout status statefulset/libredb-postgres --timeout=300s >/dev/null +for _ in $(seq 1 30); do + left=$(days_left "$(pg_not_after 2>/dev/null || true)") && [ "$left" -ge 0 ] && [ "$left" -le 10 ] && break + sleep 5 +done +{ [ "$left" -ge 0 ] && [ "$left" -le 10 ]; } || fail "PostgreSQL does not serve the short leaf: $left days left" +for _ in $(seq 1 30); do + ingress=$(days_left "$(ingress_not_after platform.libredb.test || true)") && [ "$ingress" -ge 0 ] && [ "$ingress" -le 10 ] && break + sleep 3 +done +{ [ "$ingress" -ge 0 ] && [ "$ingress" -le 10 ]; } || fail "the ingress does not serve the short leaf: $ingress days left" +echo "PostgreSQL serves a leaf with $left days left, the ingress one with $ingress" + +step "an upgrade with unchanged values re-issues both leaves and restarts PostgreSQL" +upgrade "$active" +kubectl -n "$namespace" rollout status statefulset/libredb-postgres --timeout=300s >/dev/null +for _ in $(seq 1 60); do + left=$(days_left "$(pg_not_after 2>/dev/null || true)") && [ "$left" -gt 800 ] && break + sleep 5 +done +[ "$left" -gt 800 ] || fail "PostgreSQL still serves a leaf with $left days left after the upgrade" +for _ in $(seq 1 40); do + ingress=$(days_left "$(ingress_not_after platform.libredb.test || true)") && [ "$ingress" -gt 800 ] && break + sleep 3 +done +[ "$ingress" -gt 800 ] || fail "the ingress still serves a leaf with $ingress days left" +wait_platform +curl -sf --resolve studio.libredb.test:443:127.0.0.1 --cacert "$work/ca.crt" -o /dev/null https://studio.libredb.test/api/ready || fail "Studio is not ready after PostgreSQL's restart" +echo "PostgreSQL serves a leaf with $left days left, the ingress one with $ingress; Platform and Studio ready" + +step "license lifecycle: expiring, expired, active again, each in the running Platform" +for case in "$expiring:expiring:License expiring within 30 days" "$expired:expired:License expired, no seat can be added" "$active:active:License active"; do + file=${case%%:*}; rest=${case#*:}; state=${rest%%:*}; message=${rest#*:} + upgrade "$file" + wait_platform + line=$(platform_license_line) + grep -q "\"message\":\"$message\"" <<<"$line" || fail "after the $state license Platform logged: $line" + grep -q "\"state\":\"$state\"" <<<"$line" || fail "after the $state license Platform logged: $line" + echo "$state: $(sed 's/.*"licenseId":"\([^"]*\)".*/\1/' <<<"$line")" +done + +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-offline.sh b/charts/libredb-platform/tests/e2e/run-offline.sh new file mode 100755 index 0000000..fcec9a4 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-offline.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# The install without network access of docs/deploy/install-kubernetes.md, on the kind cluster run-e2e.sh set up: +# +# LIBREDB_E2E_LICENSE=kind.license LIBREDB_E2E_MIRROR=libredb-mirror:5000 LIBREDB_E2E_MIRROR_PUSH=localhost:5055 \ +# charts/libredb-platform/tests/e2e/run-offline.sh +# +# LIBREDB_E2E_MIRROR is a registry the cluster's nodes pull from, LIBREDB_E2E_MIRROR_PUSH the same registry as this +# host reaches it. On the connected side the test packages the chart with its dependency and copies the release's +# images into the mirror by digest; on the site side Helm runs with no repository configured and every proxy pointing +# at a closed port, installs the archive with each image repository set to the mirror, and the test checks that every +# container ran an image of the mirror, the endpoints, the license and Studio's seal canary. +# LIBREDB_E2E_KEEP=1 keeps the installation. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +mirror=${LIBREDB_E2E_MIRROR:?the mirror registry as the nodes reach it} +push=${LIBREDB_E2E_MIRROR_PUSH:?the mirror registry as this host reaches it} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +step "connected side: the chart archive with its dependency" +helm package --dependency-update "$chart" -d "$work" >/dev/null +archive=$(ls "$work"/libredb-platform-*.tgz) +tar -tzf "$archive" | grep -cx 'libredb-platform/charts/libredb-studio/Chart.yaml' >/dev/null || fail "the archive lacks the Studio chart" +echo "$(basename "$archive") holds the Studio chart" + +step "connected side: the release's images into the mirror, by digest" +images=$(helm template libredb "$archive" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + | grep -oE 'image: "?[^" ]+' | awk '{print $2}' | tr -d '"' | sort -u) +for image in $images; do + # registry/repository:tag@digest, copied as repository:tag with its index, so the digest stays. + name=${image#*/} + name=${name%@*} + docker buildx imagetools create -t "$push/$name" "$image" >/dev/null 2>&1 + echo "$name" +done + +# LIBREDB_E2E_KIND_NODE: a kind node whose copies of these images go first, so its pods pull them from the mirror. +if [ -n "${LIBREDB_E2E_KIND_NODE:-}" ]; then + step "the node $LIBREDB_E2E_KIND_NODE forgets the release's images" + for image in $images; do + for ref in "$image" "$mirror/${image#*/}"; do + docker exec "$LIBREDB_E2E_KIND_NODE" crictl rmi "$ref" >/dev/null 2>&1 || true + docker exec "$LIBREDB_E2E_KIND_NODE" crictl inspecti "$ref" >/dev/null 2>&1 && fail "the node still holds $ref" + done + done + echo "none of them left on the node" +fi + +step "site side: Helm with no repository and no way out" +export HELM_REPOSITORY_CONFIG="$work/repositories.yaml" HELM_REPOSITORY_CACHE="$work/cache" HELM_REGISTRY_CONFIG="$work/registry.json" +export HTTPS_PROXY=http://127.0.0.1:9 HTTP_PROXY=http://127.0.0.1:9 https_proxy=http://127.0.0.1:9 http_proxy=http://127.0.0.1:9 +export NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost +# helm repo list fails when no repository is configured, which is what this checks. +repos=$(helm repo list 2>&1 || true) +grep -q "no repositories" <<<"$repos" || fail "Helm has a repository configured: $repos" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace "$namespace" --ignore-not-found --wait=true >/dev/null +kubectl create namespace "$namespace" >/dev/null +kubectl label namespace "$namespace" pod-security.kubernetes.io/enforce=restricted >/dev/null +mirrored=( + --set "platform.image.repository=$mirror/libredb/libredb-platform" + --set "postgresql.managed.image.repository=$mirror/library/postgres" + --set "keycloak.managed.image.repository=$mirror/keycloak/keycloak" + --set "keycloak.managed.edge.image.repository=$mirror/library/caddy" + --set "studio.image.repository=$mirror/libredb/libredb-studio" +) +rendered=$(helm template libredb "$archive" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" "${mirrored[@]}" \ + | grep -oE 'image: "?[^" ]+' | awk '{print $2}' | tr -d '"' | sort -u) +outside=$(grep -v "^$mirror/" <<<"$rendered" || true) +[ -z "$outside" ] || fail "images outside the mirror: $outside" +started=$(date +%s) +helm install libredb "$archive" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + "${mirrored[@]}" --wait --wait-for-jobs --timeout 10m >/dev/null +echo "installed from the archive in $(( $(date +%s) - started )) s, every image from $mirror" +unset HTTPS_PROXY HTTP_PROXY https_proxy http_proxy + +step "every container ran an image the nodes pulled from the mirror" +ids=$(kubectl -n "$namespace" get pods -o 'jsonpath={range .items[*]}{range .status.initContainerStatuses[*]}{.imageID}{"\n"}{end}{range .status.containerStatuses[*]}{.imageID}{"\n"}{end}{end}' | sort -u) +outside=$(grep -v "^$mirror/" <<<"$ids" || true) +[ -z "$outside" ] || fail "containers ran images from elsewhere: $outside" +sed 's/^/ /' <<<"$ids" + +step "endpoints, license and Studio's heartbeat" +kubectl -n "$namespace" get secret libredb-tls-platform -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" +for url in https://platform.libredb.test/api/health/ready https://studio.libredb.test/api/ready \ + https://auth.libredb.test/realms/libredb/.well-known/openid-configuration; do + host=${url#https://} + status=$(curl -s --resolve "${host%%/*}:443:127.0.0.1" --cacert "$work/ca.crt" -o /dev/null -w '%{http_code}' "$url") + [ "$status" = "200" ] || fail "$url answered $status" + echo "200 $url" +done +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +for _ in $(seq 1 30); do + heartbeat=$(kubectl -n "$namespace" exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -h 127.0.0.1 -U postgres -d libredb -c "SELECT canary FROM bridge.studio_instances_v1"') + grep -q '"ok"' <<<"$heartbeat" && break + sleep 5 +done +grep -q '"ok"' <<<"$heartbeat" || fail "Studio's seal canary is not ok: $heartbeat" +echo "license active; seal canary $heartbeat" + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-openshift.sh b/charts/libredb-platform/tests/e2e/run-openshift.sh new file mode 100755 index 0000000..3757be8 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-openshift.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# The end-to-end test of the Helm chart on OpenShift, in the one namespace of the current `oc login`, such as a Red +# Hat Developer Sandbox's: +# +# LIBREDB_E2E_LICENSE=os.license LIBREDB_E2E_DOMAIN=apps.example.openshiftapps.com \ +# charts/libredb-platform/tests/e2e/run-openshift.sh +# +# The hosts are ldb-platform-., ldb-studio-. and ldb-auth-., +# under the apps domain the router serves, and the license must name the first. OpenShift turns each Ingress into a +# Route that terminates TLS with the Ingress's Secret, so the endpoints answer with leaves of the internal CA. The test +# needs no cluster scope: it checks that the restricted-v2 SCC admits every pod with a uid from the namespace's range, +# the endpoints, the edge's path rules, the license and Studio's seal canary, runs the browser suite, upgrades, rolls +# back, uninstalls and installs again, as run-e2e.sh does on kind. It removes everything the chart left in the +# namespace before it starts and when it ends (LIBREDB_E2E_KEEP=1 keeps the last installation), and nothing else. +# Needs oc, kubectl, helm, curl and bun on PATH. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for ldb-platform-.} +domain=${LIBREDB_E2E_DOMAIN:?set LIBREDB_E2E_DOMAIN to the apps domain of the router} +namespace=$(oc project -q) +platform=ldb-platform-$namespace.$domain +studio=ldb-studio-$namespace.$domain +auth=ldb-auth-$namespace.$domain +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +sed -e "s/ldb-platform\.apps\.example\.com/$platform/g" -e "s/ldb-studio\.apps\.example\.com/$studio/g" \ + -e "s/ldb-auth\.apps\.example\.com/$auth/g" "$here/values-openshift.yaml" > "$work/values.yaml" + +install_chart() { + helm upgrade --install libredb "$chart" -n "$namespace" -f "$work/values.yaml" \ + --set-file platform.license.value="$license" "$@" --wait --wait-for-jobs --timeout 15m +} + +secret_hash() { kubectl -n "$namespace" get secret "$1" -o "jsonpath={.data.$2}" | sha256sum | cut -d' ' -f1; } + +# What the chart leaves on purpose after helm uninstall: the jobs' Secrets and ConfigMaps and the volumes. +remove_installation() { + helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true + kubectl -n "$namespace" delete pod/e2e-target service/e2e-target --ignore-not-found >/dev/null + kubectl -n "$namespace" delete secret,configmap -l app.kubernetes.io/managed-by=libredb-bootstrap --ignore-not-found >/dev/null + kubectl -n "$namespace" delete pvc -l app.kubernetes.io/instance=libredb --ignore-not-found --wait=true >/dev/null +} + +step "a clean namespace $namespace" +remove_installation + +step "install with OpenShift's restricted-v2 SCC" +started=$(date +%s) +install_chart >/dev/null +echo "installed in $(( $(date +%s) - started )) s" +errors=$(kubectl -n "$namespace" get pods --no-headers | awk '$3 == "Error" || $3 == "CrashLoopBackOff"' | wc -l) +[ "$errors" -eq 0 ] || fail "$errors pods failed during the install" + +step "every pod runs under restricted-v2 with a uid from the namespace's range" +range=$(oc get namespace "$namespace" -o 'jsonpath={.metadata.annotations.openshift\.io/sa\.scc\.uid-range}') +first=${range%/*} +size=${range#*/} +pods=$(kubectl -n "$namespace" get pods -l app.kubernetes.io/instance=libredb --field-selector=status.phase=Running -o name) +[ -n "$pods" ] || fail "no running pod of the release" +for pod in $pods; do + scc=$(kubectl -n "$namespace" get "$pod" -o 'jsonpath={.metadata.annotations.openshift\.io/scc}') + [ "$scc" = "restricted-v2" ] || fail "$pod runs under SCC $scc" + # The SCC writes the uid it assigned into each container's security context. + uid=$(kubectl -n "$namespace" get "$pod" -o 'jsonpath={.spec.containers[0].securityContext.runAsUser}') + [ "$uid" -ge "$first" ] && [ "$uid" -lt $(( first + size )) ] || fail "$pod runs as uid $uid, outside $range" + printf '%s restricted-v2 uid %s\n' "$pod" "$uid" +done + +step "the Routes OpenShift made of the Ingresses" +for host in "$platform" "$studio" "$auth"; do + termination=$(kubectl -n "$namespace" get routes -o "jsonpath={.items[?(@.spec.host=='$host')].spec.tls.termination}") + [ "$termination" = "edge" ] || fail "no edge Route for $host" +done +echo "edge Routes for the three hosts" + +step "endpoints, the edge's path rules and TLS against the internal CA" +kubectl -n "$namespace" get secret libredb-tls-platform -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" +expect_status() { + local status + status=$(curl -s --cacert "$work/ca.crt" --path-as-is -o /dev/null -w '%{http_code}' "$2") + [ "$status" = "$1" ] || fail "$2 answered $status, not $1" + printf '%s %s\n' "$status" "$2" +} +expect_status 200 "https://$platform/api/health/ready" +expect_status 200 "https://$studio/api/ready" +expect_status 200 "https://$auth/realms/libredb/.well-known/openid-configuration" +expect_status 404 "https://$auth/admin/" +expect_status 404 "https://$auth/realms/master/" +expect_status 400 "https://$auth/realms/libredb/..;/master/" + +step "license and Studio's heartbeat" +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +for _ in $(seq 1 30); do + heartbeat=$(kubectl -n "$namespace" exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -h 127.0.0.1 -U postgres -d libredb -c "SELECT canary FROM bridge.studio_instances_v1"') + grep -q '"ok"' <<<"$heartbeat" && break + sleep 5 +done +grep -q '"ok"' <<<"$heartbeat" || fail "Studio's seal canary is not ok: $heartbeat" +echo "license active; seal canary $heartbeat" + +step "browser suite" +license_id=$(cut -d. -f3 "$license" | tr '_-' '/+' | base64 -d 2>/dev/null | grep -o '"id":"[^"]*"' | cut -d'"' -f4 || true) +(cd "$installer" && LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_OPENSHIFT=1 \ + LIBREDB_E2E_PLATFORM_URL="https://$platform" LIBREDB_E2E_STUDIO_URL="https://$studio" \ + LIBREDB_E2E_LICENSE_ID="$license_id" LIBREDB_E2E_ACCOUNT_FILE="${LIBREDB_E2E_ACCOUNT_FILE:-$work/first-admin.json}" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts) + +step "upgrade with a changed value keeps the seal keys and the CA" +seal=$(secret_hash libredb-secrets 'seal-keys\.json') +ca=$(secret_hash libredb-tls 'ca\.crt') +install_chart --set platform.queryLogRetentionDays=30 >/dev/null +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the upgrade changed the seal keys" +[ "$(secret_hash libredb-tls 'ca\.crt')" = "$ca" ] || fail "the upgrade changed the internal CA" +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=180s >/dev/null +running=$(kubectl -n "$namespace" exec deploy/libredb-platform -- printenv QUERY_LOG_RETENTION_DAYS) +[ "$running" = "30" ] || fail "the running Platform has QUERY_LOG_RETENTION_DAYS=$running after the upgrade" +expect_status 200 "https://$platform/api/health/ready" +echo "seal keys and CA kept, QUERY_LOG_RETENTION_DAYS=30 in the running server" + +step "a rollback within the release restores the earlier values" +helm -n "$namespace" rollback libredb --wait --timeout 15m >/dev/null +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=180s >/dev/null +running=$(kubectl -n "$namespace" exec deploy/libredb-platform -- printenv QUERY_LOG_RETENTION_DAYS) +[ "$running" = "90" ] || fail "the running Platform has QUERY_LOG_RETENTION_DAYS=$running after the rollback" +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the rollback changed the seal keys" +echo "rolled back to QUERY_LOG_RETENTION_DAYS=90, seal keys kept" + +step "uninstall keeps the generated Secrets and the volumes; a new install reuses them" +helm -n "$namespace" uninstall libredb --wait >/dev/null +for name in libredb-secrets libredb-tls libredb-installer-state; do + kubectl -n "$namespace" get secret "$name" >/dev/null || fail "uninstall removed Secret $name" +done +kubectl -n "$namespace" get pvc data-libredb-postgres-0 >/dev/null || fail "uninstall removed the database volume" +install_chart >/dev/null +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the reinstall changed the seal keys" +[ "$(secret_hash libredb-tls 'ca\.crt')" = "$ca" ] || fail "the reinstall changed the internal CA" +kubectl -n "$namespace" logs -l app.kubernetes.io/component=keycloak-configure --tail=20 | grep -c "realm already converged" >/dev/null \ + || fail "the reinstall's keycloak job did not find the realm converged" +expect_status 200 "https://$platform/api/health/ready" +expect_status 200 "https://$studio/api/ready" +echo "reinstall reused the seal keys, the CA, the database and the realm" + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + remove_installation +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-resilience.sh b/charts/libredb-platform/tests/e2e/run-resilience.sh new file mode 100755 index 0000000..a7303d4 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-resilience.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# The resilience test of the Helm chart, on the kind cluster run-e2e.sh set up: +# +# LIBREDB_E2E_LICENSE=kind.license charts/libredb-platform/tests/e2e/run-resilience.sh +# +# It installs the chart, and the browser suite restore.k8s.ts (phase seed) has the first admin connect a target +# PostgreSQL and grant a Keycloak user, who reads the table in Studio. Then it kills, without a grace period, the +# PostgreSQL pod, then Platform, Studio and Keycloak at once, and finally restarts the kind node itself, the cluster's +# power cut. After each it waits, with no other action, until the three endpoints answer again, reports how long that +# took, and checks that the connections, the grants and the Keycloak users are what they were. The browser suite +# (phase verify) then signs the first admin in with the seed's password and TOTP device and has the user read the table +# through the credential sealed in the seed. LIBREDB_E2E_KEEP=1 keeps the installation. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +node=${LIBREDB_E2E_KIND_NODE:-libredb-helm-control-plane} +target_namespace=libredb-resilience-target +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +status() { curl -s --max-time 5 --resolve "$1:443:127.0.0.1" --cacert "$work/ca.crt" -o /dev/null -w '%{http_code}' "https://$1$2" || true; } +answering() { + [ "$(status platform.libredb.test /api/health/ready)" = 200 ] && [ "$(status studio.libredb.test /api/ready)" = 200 ] \ + && [ "$(status auth.libredb.test /realms/libredb/.well-known/openid-configuration)" = 200 ] +} +# Waits, doing nothing else, until the three endpoints answer 200, and prints how long that took since $1. +recovered() { + local since=$1 + for _ in $(seq 1 240); do + if answering; then echo "every endpoint answers again $(( $(date +%s) - since )) s after it"; return; fi + sleep 5 + done + fail "the endpoints did not answer within 20 minutes" +} + +psql_super() { + kubectl -n "$namespace" exec -i libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -q -h 127.0.0.1 -U postgres -d "$1"' sh "$1" +} +# What a crash must not lose: the connections with their envelopes, the grants, and the realm's users. +fingerprint() { + { + echo "SELECT md5(coalesce(string_agg(t::text, '|' ORDER BY t::text), '')) FROM (SELECT id, \"readSecret\", \"writeSecret\" FROM platform.database_connections) t; + SELECT md5(coalesce(string_agg(t::text, '|' ORDER BY t::text), '')) FROM platform.connection_grants t;" | psql_super libredb + echo "SELECT md5(coalesce(string_agg(username, '|' ORDER BY username), '')) FROM user_entity;" | psql_super keycloak + } | tr '\n' ' ' +} + +browser() { + (cd "$installer" && LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_TARGET_NAMESPACE="$target_namespace" \ + LIBREDB_E2E_RESTORE_PHASE="$1" LIBREDB_E2E_RESTORE_STATE="$work/state.json" LIBREDB_E2E_ACCOUNT_FILE="$work/first-admin.json" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts restore.k8s.ts) +} + +step "a fresh installation in namespace $namespace, the target database in $target_namespace" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace "$namespace" "$target_namespace" --ignore-not-found --wait=true >/dev/null +for ns in "$namespace" "$target_namespace"; do + kubectl create namespace "$ns" >/dev/null + kubectl label namespace "$ns" pod-security.kubernetes.io/enforce=restricted >/dev/null +done +helm install libredb "$chart" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + --wait --wait-for-jobs --timeout 10m >/dev/null +kubectl -n "$namespace" get secret libredb-tls-platform -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" + +step "seed: a connection, a grant and a read in Studio" +browser seed +before=$(fingerprint) +echo "fingerprint $before" + +step "PostgreSQL killed without a grace period" +started=$(date +%s) +kubectl -n "$namespace" delete pod libredb-postgres-0 --grace-period=0 --force >/dev/null 2>&1 +recovered "$started" +[ "$(fingerprint)" = "$before" ] || fail "the data changed: $(fingerprint)" + +step "Platform, Studio and Keycloak killed at once" +started=$(date +%s) +kubectl -n "$namespace" delete pod -l 'app.kubernetes.io/component in (platform,keycloak)' --grace-period=0 --force >/dev/null 2>&1 +kubectl -n "$namespace" delete pod -l app.kubernetes.io/name=studio --grace-period=0 --force >/dev/null 2>&1 +recovered "$started" +[ "$(fingerprint)" = "$before" ] || fail "the data changed: $(fingerprint)" + +step "the node $node restarted, the cluster's power cut" +started=$(date +%s) +docker restart "$node" >/dev/null +for _ in $(seq 1 120); do kubectl get nodes >/dev/null 2>&1 && break; sleep 5; done +kubectl wait --for=condition=Ready "node/$node" --timeout=600s >/dev/null +recovered "$started" +[ "$(fingerprint)" = "$before" ] || fail "the data changed: $(fingerprint)" +restarts=$(kubectl -n "$namespace" get pods -o 'jsonpath={range .items[*]}{.metadata.name}={range .status.containerStatuses[*]}{.restartCount}{" "}{end}{"\n"}{end}') +printf 'container restarts after the power cut:\n%s\n' "$restarts" + +step "verify: the first admin signs in, and the user reads the table through the credential of the seed" +browser verify + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" "$target_namespace" --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-restore.sh b/charts/libredb-platform/tests/e2e/run-restore.sh new file mode 100755 index 0000000..81b7847 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-restore.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# The backup and restore test of the Helm chart, on the kind cluster run-e2e.sh set up, with the steps of "Back up and +# restore" in docs/deploy/install-kubernetes.md: +# +# LIBREDB_E2E_LICENSE=kind.license charts/libredb-platform/tests/e2e/run-restore.sh +# +# It installs the chart, and the browser suite restore.k8s.ts (phase seed) has the first admin connect a target +# PostgreSQL in a namespace of its own and grant a Keycloak user, who reads the table in Studio. Then it takes the +# backup set of the guide (the three Secrets and a pg_dump of each database), deletes the installation's namespace, +# volumes included, restores the set into a new namespace of the same name and checks the seal keys, the CA, the +# license and Studio's seal canary; the browser suite (phase verify) then signs the first admin in with the seed's +# password and TOTP device and has the user read the table again, through the credential sealed before the backup. +# LIBREDB_E2E_KEEP=1 keeps the restored installation and the target database. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +target_namespace=libredb-restore-target +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +backup="$work/backup" + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +secret_hash() { kubectl -n "$namespace" get secret "$1" -o "jsonpath={.data.$2}" | sha256sum | cut -d' ' -f1; } + +install_chart() { + helm upgrade --install libredb "$chart" -n "$namespace" -f "$here/values-kind.yaml" \ + --set-file platform.license.value="$license" --wait --wait-for-jobs --timeout 10m +} + +restricted_namespace() { + kubectl create namespace "$1" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + kubectl label namespace "$1" pod-security.kubernetes.io/enforce=restricted --overwrite >/dev/null +} + +browser() { + (cd "$installer" && LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_TARGET_NAMESPACE="$target_namespace" \ + LIBREDB_E2E_RESTORE_PHASE="$1" LIBREDB_E2E_RESTORE_STATE="$work/state.json" LIBREDB_E2E_ACCOUNT_FILE="$work/first-admin.json" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts restore.k8s.ts) +} + +databases=(libredb keycloak) + +step "a fresh installation in namespace $namespace, the target database in $target_namespace" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace "$namespace" "$target_namespace" --ignore-not-found --wait=true >/dev/null +restricted_namespace "$namespace" +restricted_namespace "$target_namespace" +install_chart >/dev/null + +step "seed: a connection, a grant and a read in Studio" +browser seed + +step "the backup set of the guide" +mkdir -m 0700 "$backup" +kubectl -n "$namespace" get secret libredb-secrets libredb-tls libredb-installer-state -o yaml > "$backup/secrets.yaml" +for db in "${databases[@]}"; do + kubectl -n "$namespace" exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -h 127.0.0.1 -U postgres -Fc -d "$1"' sh "$db" > "$backup/$db.dump" +done +seal=$(secret_hash libredb-secrets 'seal-keys\.json') +ca=$(secret_hash libredb-tls 'ca\.crt') +ls -l "$backup" | tail -n +2 + +step "the disaster: namespace $namespace deleted, its volumes with it" +helm -n "$namespace" uninstall libredb --wait >/dev/null +kubectl delete namespace "$namespace" --wait=true >/dev/null +volumes() { kubectl get pv -o 'jsonpath={range .items[*]}{.spec.claimRef.namespace}{"\n"}{end}' | grep -cx "$namespace" || true; } +for _ in $(seq 1 60); do [ "$(volumes)" = "0" ] && break; sleep 2; done +[ "$(volumes)" = "0" ] || fail "a volume of $namespace survived" +echo "namespace and volumes gone" + +step "restore: the Secrets, a new install, then each database while the apps are stopped" +restricted_namespace "$namespace" +kubectl -n "$namespace" create -f "$backup/secrets.yaml" >/dev/null +install_chart >/dev/null +kubectl -n "$namespace" scale deployment libredb-platform libredb-studio keycloak --replicas=0 >/dev/null +kubectl -n "$namespace" wait --for=delete pod -l 'app.kubernetes.io/component in (platform,keycloak)' --timeout=180s >/dev/null +kubectl -n "$namespace" wait --for=delete pod -l app.kubernetes.io/name=studio --timeout=180s >/dev/null +for db in "${databases[@]}"; do + kubectl -n "$namespace" exec -i libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" pg_restore -h 127.0.0.1 -U postgres --clean --if-exists --exit-on-error -d "$1"' sh "$db" \ + < "$backup/$db.dump" +done +kubectl -n "$namespace" scale deployment libredb-platform libredb-studio keycloak --replicas=1 >/dev/null +for deploy in libredb-platform libredb-studio keycloak; do + kubectl -n "$namespace" rollout status "deploy/$deploy" --timeout=300s >/dev/null +done + +step "the restored installation" +[ "$(secret_hash libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the restore changed the seal keys" +[ "$(secret_hash libredb-tls 'ca\.crt')" = "$ca" ] || fail "the restore changed the internal CA" +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform did not log License active" +for _ in $(seq 1 30); do + heartbeat=$(kubectl -n "$namespace" exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -h 127.0.0.1 -U postgres -d libredb -c "SELECT canary FROM bridge.studio_instances_v1"') + grep -q '"ok"' <<<"$heartbeat" && break + sleep 5 +done +grep -q '"ok"' <<<"$heartbeat" || fail "Studio's seal canary is not ok: $heartbeat" +echo "seal keys and CA of the backup, license active, seal canary ok" + +step "verify: the first admin of the backup signs in, and the user reads the table through the restored credential" +browser verify + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" "$target_namespace" --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-rotation.sh b/charts/libredb-platform/tests/e2e/run-rotation.sh new file mode 100755 index 0000000..546a055 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-rotation.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# The rotation test of the Helm chart, on the kind cluster run-e2e.sh set up: a fresh installation, then the browser +# suite rotation.k8s.ts, which walks the seal key rotation and the secret rotations of "Rotate secrets and the seal +# key" in docs/deploy/install-kubernetes.md, one helm upgrade per step: +# +# LIBREDB_E2E_LICENSE=kind.license charts/libredb-platform/tests/e2e/run-rotation.sh +# +# LIBREDB_E2E_KEEP=1 keeps the installation. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +target_namespace=libredb-rotation-target +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +step() { printf '\n=== %s\n' "$*"; } + +step "a fresh installation in namespace $namespace, the target database in $target_namespace" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace "$namespace" "$target_namespace" --ignore-not-found --wait=true >/dev/null +for ns in "$namespace" "$target_namespace"; do + kubectl create namespace "$ns" >/dev/null + kubectl label namespace "$ns" pod-security.kubernetes.io/enforce=restricted >/dev/null +done +helm install libredb "$chart" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + --wait --wait-for-jobs --timeout 10m >/dev/null + +step "the rotations, one helm upgrade per step" +(cd "$installer" && LIBREDB_E2E_ROTATION=1 LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_TARGET_NAMESPACE="$target_namespace" \ + LIBREDB_E2E_CHART="$chart" LIBREDB_E2E_VALUES="$here/values-kind.yaml" LIBREDB_E2E_LICENSE="$license" \ + LIBREDB_E2E_ACCOUNT_FILE="$work/first-admin.json" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts rotation.k8s.ts) + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" "$target_namespace" --wait=true >/dev/null +fi +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-soak.sh b/charts/libredb-platform/tests/e2e/run-soak.sh new file mode 100755 index 0000000..68dcb0d --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-soak.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# The soak test of the Helm chart, on the kind cluster run-e2e.sh set up: one installation left running for hours. +# +# LIBREDB_E2E_LICENSE=kind.license LIBREDB_E2E_SOAK_HOURS=8 LIBREDB_E2E_SOAK_OUT=/dir \ +# charts/libredb-platform/tests/e2e/run-soak.sh +# +# After the seed of restore.k8s.ts it checks the three endpoints every 30 seconds, records every 5 minutes each +# container's memory and CPU from the node's CRI, the restart counts, the databases' sizes and the size of the pods' +# logs, and every 30 minutes has the browser suite (phase verify) sign the first admin in and the user read the table. +# The CSV files and summary.txt land in LIBREDB_E2E_SOAK_OUT. It fails when a verify run fails or a container restarts; +# availability, latency, memory and growth go to the summary. LIBREDB_E2E_KEEP=1 keeps the installation. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +installer=$(cd "$chart/../../installer" && pwd) +license=${LIBREDB_E2E_LICENSE:?set LIBREDB_E2E_LICENSE to a license file for platform.libredb.test} +hours=${LIBREDB_E2E_SOAK_HOURS:-8} +# LIBREDB_E2E_SOAK_MINUTES, for a short trial run, wins over the hours. +seconds=$(( ${LIBREDB_E2E_SOAK_MINUTES:-$(( hours * 60 ))} * 60 )) +out=${LIBREDB_E2E_SOAK_OUT:?a directory for the measurements} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} +node=${LIBREDB_E2E_KIND_NODE:-libredb-helm-control-plane} +target_namespace=libredb-soak-target +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$out" + +step() { printf '\n=== %s\n' "$*"; } +now() { date -u +%Y-%m-%dT%H:%M:%SZ; } + +browser() { + (cd "$installer" && LIBREDB_E2E_NAMESPACE="$namespace" LIBREDB_E2E_TARGET_NAMESPACE="$target_namespace" \ + LIBREDB_E2E_RESTORE_PHASE="$1" LIBREDB_E2E_RESTORE_STATE="$work/state.json" LIBREDB_E2E_ACCOUNT_FILE="$work/first-admin.json" \ + bunx playwright test --config e2e/kubernetes/playwright.config.ts restore.k8s.ts) +} +psql_super() { + kubectl -n "$namespace" exec -i libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" psql -X -A -t -q -h 127.0.0.1 -U postgres -d libredb' +} + +step "a fresh installation in namespace $namespace, the target database in $target_namespace" +helm -n "$namespace" uninstall libredb --wait >/dev/null 2>&1 || true +kubectl delete namespace "$namespace" "$target_namespace" --ignore-not-found --wait=true >/dev/null +for ns in "$namespace" "$target_namespace"; do + kubectl create namespace "$ns" >/dev/null + kubectl label namespace "$ns" pod-security.kubernetes.io/enforce=restricted >/dev/null +done +helm install libredb "$chart" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + --wait --wait-for-jobs --timeout 10m >/dev/null +kubectl -n "$namespace" get secret libredb-tls-platform -o 'jsonpath={.data.ca\.crt}' | base64 -d > "$work/ca.crt" +browser seed + +echo "time,endpoint,status,seconds" > "$out/endpoints.csv" +echo "time,pod,container,memory_bytes,cpu_nanocores" > "$out/memory.csv" +echo "time,restarts,libredb_db_bytes,keycloak_db_bytes,pod_log_bytes" > "$out/growth.csv" +echo "time,verify" > "$out/verify.csv" + +probe() { + local host=$1 path=$2 result + result=$(curl -s --max-time 20 --resolve "$host:443:127.0.0.1" --cacert "$work/ca.crt" -o /dev/null -w '%{http_code},%{time_total}' "https://$host$path" || echo "000,20") + echo "$(now),$host,$result" >> "$out/endpoints.csv" +} +sample() { + local pods + pods=$(docker exec "$node" crictl pods --namespace "$namespace" -q | tr '\n' ' ') + for pod in $pods; do + docker exec "$node" crictl stats -p "$pod" -o json 2>/dev/null | python3 -I -c ' +import json, sys +stats = json.load(sys.stdin).get("stats", []) +for s in stats: + attrs = s["attributes"] + pod = attrs["labels"].get("io.kubernetes.pod.name", "") + mem = s.get("memory", {}).get("workingSetBytes", {}).get("value", 0) + # crictl matches the namespace as a pattern, and a finished job container reports nothing. + if attrs["labels"].get("io.kubernetes.pod.namespace") != sys.argv[2] or mem == 0: + continue + cpu = s.get("cpu", {}).get("usageNanoCores", {}).get("value", 0) + name = attrs["metadata"]["name"] + print(f"{sys.argv[1]},{pod},{name},{mem},{cpu}") +' "$(now)" "$namespace" >> "$out/memory.csv" + done + local restarts sizes logs + restarts=$(kubectl -n "$namespace" get pods -o 'jsonpath={range .items[*]}{range .status.containerStatuses[*]}{.restartCount}{"\n"}{end}{end}' | awk '{s+=$1} END {print s+0}') + sizes=$(echo "SELECT pg_database_size('libredb') || ',' || pg_database_size('keycloak')" | psql_super) + logs=$(docker exec "$node" sh -c "du -sb /var/log/pods/${namespace}_* 2>/dev/null | awk '{s+=\$1} END {print s+0}'") + echo "$(now),$restarts,$sizes,$logs" >> "$out/growth.csv" +} + +step "soak for $(( seconds / 60 )) minutes" +end=$(( $(date +%s) + seconds )) +next_sample=0 +# Every 30 minutes, or twice in a run shorter than an hour. +verify_every=$(( seconds < 3600 ? seconds / 2 : 1800 )) +next_verify=$(( $(date +%s) + verify_every )) +failures=0 +while [ "$(date +%s)" -lt "$end" ]; do + probe platform.libredb.test /api/health/ready + probe studio.libredb.test /api/ready + probe auth.libredb.test /realms/libredb/.well-known/openid-configuration + if [ "$(date +%s)" -ge "$next_sample" ]; then + sample || echo "$(now) sample failed" + next_sample=$(( $(date +%s) + 300 )) + fi + if [ "$(date +%s)" -ge "$next_verify" ]; then + if browser verify > "$work/verify.log" 2>&1; then echo "$(now),pass" >> "$out/verify.csv"; else + echo "$(now),fail" >> "$out/verify.csv"; failures=$((failures + 1)); cp "$work/verify.log" "$out/verify-fail-$(date +%s).log" + fi + next_verify=$(( $(date +%s) + verify_every )) + fi + sleep 30 +done +sample + +step "summary" +python3 -I - "$out" > "$out/summary.txt" <<'EOF' +import csv, sys, statistics, collections +out = sys.argv[1] +rows = list(csv.DictReader(open(f"{out}/endpoints.csv"))) +print(f"endpoint checks: {len(rows)}") +for host in sorted({r["endpoint"] for r in rows}): + mine = [r for r in rows if r["endpoint"] == host] + ok = [r for r in mine if r["status"] == "200"] + times = sorted(float(r["seconds"]) for r in ok) + p99 = times[int(len(times) * 0.99) - 1] if times else 0 + print(f" {host}: {len(ok)}/{len(mine)} answered 200 ({100 * len(ok) / max(len(mine), 1):.3f}%), median {statistics.median(times) if times else 0:.3f} s, p99 {p99:.3f} s, max {max(times) if times else 0:.3f} s") +mem = list(csv.DictReader(open(f"{out}/memory.csv"))) +by = collections.defaultdict(list) +for r in mem: + by[r["container"]].append(int(r["memory_bytes"])) +print("memory, first hour average -> last hour average (MiB):") +for name, values in sorted(by.items()): + first, last = values[:12], values[-12:] + a, b = statistics.mean(first) / 2**20, statistics.mean(last) / 2**20 + print(f" {name}: {a:.0f} -> {b:.0f} ({100 * (b - a) / a if a else 0:+.1f}%), peak {max(values) / 2**20:.0f}") +growth = list(csv.DictReader(open(f"{out}/growth.csv"))) +if growth: + f, l = growth[0], growth[-1] + print(f"restarts: {f['restarts']} -> {l['restarts']}") + for key in ("libredb_db_bytes", "keycloak_db_bytes", "pod_log_bytes"): + print(f"{key}: {int(f[key]) / 2**20:.1f} MiB -> {int(l[key]) / 2**20:.1f} MiB") +verify = list(csv.DictReader(open(f"{out}/verify.csv"))) +print(f"browser verify runs: {sum(r['verify'] == 'pass' for r in verify)} passed, {sum(r['verify'] == 'fail' for r in verify)} failed") +EOF +cat "$out/summary.txt" +restarts=$(tail -1 "$out/growth.csv" | cut -d, -f2) + +if [ "${LIBREDB_E2E_KEEP:-0}" != "1" ]; then + step "cleanup" + helm -n "$namespace" uninstall libredb --wait >/dev/null + kubectl delete namespace "$namespace" "$target_namespace" --wait=true >/dev/null +fi +[ "$failures" -eq 0 ] || { echo "FAIL: $failures browser verify runs failed"; exit 1; } +[ "$restarts" = "0" ] || { echo "FAIL: containers restarted $restarts times"; exit 1; } +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/run-upgrade.sh b/charts/libredb-platform/tests/e2e/run-upgrade.sh new file mode 100755 index 0000000..b67ccb7 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/run-upgrade.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# The release upgrade test, on the installation run-e2e.sh left running (LIBREDB_E2E_KEEP=1), with this chart as the +# installed release and a chart of a later release: +# +# LIBREDB_E2E_LICENSE=kind.license LIBREDB_E2E_NEXT_CHART=/path/to/next/charts/libredb-platform \ +# charts/libredb-platform/tests/e2e/run-upgrade.sh +# +# First an upgrade to the next release that fails after its hooks (Studio's image does not exist) under --atomic +# must roll back to this release, which the installation still counts as its own. Then it upgrades to the next release and checks that the +# migrate job and Platform run the next image, the installation records the next release, the seal keys and the CA +# stay, and Platform and Studio keep working; then that going back, by an upgrade with this chart or by helm rollback, +# is refused at once by the bootstrap job and leaves the next release running, and that the documented recovery from +# the refused rollback's pending record lets the next upgrade through. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +chart=$(cd "$here/../.." && pwd) +next=${LIBREDB_E2E_NEXT_CHART:?the chart directory of the next release} +license=${LIBREDB_E2E_LICENSE:?a license for platform.libredb.test} +namespace=${LIBREDB_E2E_NAMESPACE:-libredb} + +step() { printf '\n=== %s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +release() { kubectl -n "$namespace" get secret libredb-installer-state -o 'jsonpath={.data.release}' | base64 -d; } +hash_of() { kubectl -n "$namespace" get secret "$1" -o "jsonpath={.data.$2}" | sha256sum | cut -d' ' -f1; } +image() { kubectl -n "$namespace" get deploy libredb-platform -o 'jsonpath={.spec.template.spec.containers[0].image}'; } +ready() { curl -s --resolve "$1:443:127.0.0.1" -k -o /dev/null -w '%{http_code}' "https://$1$2"; } +deploy() { + helm upgrade libredb "$1" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + --wait --wait-for-jobs --timeout 10m +} + +next_version=$(awk '/^appVersion:/{gsub(/"/,"",$2); print $2}' "$next/Chart.yaml") +this_version=$(awk '/^appVersion:/{gsub(/"/,"",$2); print $2}' "$chart/Chart.yaml") +seal=$(hash_of libredb-secrets 'seal-keys\.json') +ca=$(hash_of libredb-tls 'ca\.crt') +[ "$(release)" = "$this_version" ] || fail "the installation runs $(release), not $this_version" + +step "a failed upgrade under --atomic rolls back to $this_version" +if helm upgrade libredb "$next" -n "$namespace" -f "$here/values-kind.yaml" --set-file platform.license.value="$license" \ + --set studio.image.tag=does-not-exist --atomic --wait --wait-for-jobs --timeout 3m > /tmp/atomic.log 2>&1; then + fail "the upgrade to a missing image succeeded" +fi +grep -q "rolled back\|rollback" /tmp/atomic.log || fail "--atomic did not roll back: $(tail -2 /tmp/atomic.log)" +status=$(helm -n "$namespace" status libredb -o json | node -e 'let t="";process.stdin.on("data",c=>t+=c).on("end",()=>console.log(JSON.parse(t).info.status))') +[ "$status" = "deployed" ] || fail "the release is $status after the atomic rollback" +case "$(image)" in *":$this_version"*) ;; *) fail "Platform runs $(image) after the atomic rollback" ;; esac +[ "$(release)" = "$this_version" ] || fail "the installation records $(release) after the atomic rollback" +for _ in $(seq 1 60); do [ "$(ready platform.libredb.test /api/health/ready)" = "200" ] && break; sleep 3; done +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready after the atomic rollback" +echo "rolled back to $this_version, release deployed, Platform ready" + +step "upgrade $this_version -> $next_version" +deploy "$next" >/dev/null +kubectl -n "$namespace" rollout status deploy/libredb-platform --timeout=300s >/dev/null +case "$(image)" in *":$next_version"*) ;; *) fail "Platform runs $(image) after the upgrade" ;; esac +migrate=$(kubectl -n "$namespace" get jobs -l app.kubernetes.io/component=platform-migrate -o 'jsonpath={.items[*].spec.template.spec.containers[0].image}') +case "$migrate" in *":$next_version"*) ;; *) fail "the migrate job ran $migrate" ;; esac +[ "$(release)" = "$next_version" ] || fail "the installation records $(release) after the upgrade" +[ "$(hash_of libredb-secrets 'seal-keys\.json')" = "$seal" ] || fail "the upgrade changed the seal keys" +[ "$(hash_of libredb-tls 'ca\.crt')" = "$ca" ] || fail "the upgrade changed the internal CA" +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready on $next_version" +[ "$(ready studio.libredb.test /api/ready)" = "200" ] || fail "Studio is not ready on $next_version" +kubectl -n "$namespace" logs deploy/libredb-platform | grep -c '"message":"License active"' >/dev/null || fail "Platform $next_version did not log License active" +echo "Platform and its migrate job run $next_version, the installation records it, seal keys and CA kept" + +step "an upgrade back to $this_version is refused" +if deploy "$chart" > /tmp/downgrade.log 2>&1; then fail "the downgrade to $this_version succeeded"; fi +kubectl -n "$namespace" logs job/libredb-bootstrap 2>/dev/null | grep -c "a downgrade is not supported" >/dev/null \ + || grep -q "downgrade is not supported\|pre-upgrade hooks failed" /tmp/downgrade.log || fail "the downgrade failed for another reason: $(tail -3 /tmp/downgrade.log)" +case "$(image)" in *":$next_version"*) ;; *) fail "Platform runs $(image) after the refused downgrade" ;; esac +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready after the refused downgrade" +echo "refused: $(kubectl -n "$namespace" logs job/libredb-bootstrap 2>/dev/null | grep -o 'this chart runs LibreDB [^:]*' | head -1)" + +step "helm rollback to the $this_version revision is refused" +target=$(helm -n "$namespace" history libredb -o json | node -e ' + let text = ""; process.stdin.on("data", (chunk) => (text += chunk)).on("end", () => { + const deployed = JSON.parse(text).filter((r) => r.app_version === process.argv[1] && r.status !== "failed"); + console.log(deployed.length === 0 ? "" : deployed[deployed.length - 1].revision); + });' "$this_version") +[ -n "$target" ] && [ "$target" != "null" ] || fail "no deployed revision of $this_version to roll back to" +if helm -n "$namespace" rollback libredb "$target" --wait --timeout 10m > /tmp/rollback.log 2>&1; then fail "the rollback to $this_version succeeded"; fi +grep -q "PodFailurePolicy\|pre-rollback hooks failed" /tmp/rollback.log || fail "the rollback failed for another reason: $(tail -3 /tmp/rollback.log)" +kubectl -n "$namespace" logs job/libredb-bootstrap | grep -c "a downgrade is not supported" >/dev/null || fail "the rollback's bootstrap failed for another reason" +case "$(image)" in *":$next_version"*) ;; *) fail "Platform runs $(image) after the refused rollback" ;; esac +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready after the refused rollback" +echo "refused at once; Platform still runs $next_version" + +step "the documented recovery from the refused rollback's pending record" +pending=$(helm -n "$namespace" history libredb -o json | node -e 'let t="";process.stdin.on("data",c=>t+=c).on("end",()=>{const p=JSON.parse(t).filter(r=>r.status==="pending-rollback");console.log(p.length?p[p.length-1].revision:"")})') +[ -n "$pending" ] || fail "Helm left no pending-rollback record, so the recovery step of the docs is stale" +kubectl -n "$namespace" delete secret "sh.helm.release.v1.libredb.v$pending" >/dev/null +deploy "$next" >/dev/null || fail "the upgrade after the recovery failed" +[ "$(ready platform.libredb.test /api/health/ready)" = "200" ] || fail "Platform is not ready after the recovery" +echo "removed the pending record v$pending, and the next upgrade went through" + +printf '\nPASS\n' diff --git a/charts/libredb-platform/tests/e2e/traefik-values.yaml b/charts/libredb-platform/tests/e2e/traefik-values.yaml new file mode 100644 index 0000000..86d7db2 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/traefik-values.yaml @@ -0,0 +1,23 @@ +# The ingress controller of the kind end-to-end test: Traefik on the node ports that kind-config.yaml maps to +# 127.0.0.1:80 and 127.0.0.1:443 of the host. +ingressClass: + enabled: true + isDefaultClass: true +service: + spec: + type: NodePort +ports: + web: + nodePort: 30080 + websecure: + nodePort: 30443 +providers: + kubernetesIngress: + enabled: true + # Publish an address on every Ingress, as a cloud load balancer does: Argo CD counts an Ingress healthy only then. + ingressEndpoint: + ip: 127.0.0.1 + publishedService: + enabled: false + kubernetesCRD: + enabled: false diff --git a/charts/libredb-platform/tests/e2e/values-kind.yaml b/charts/libredb-platform/tests/e2e/values-kind.yaml new file mode 100644 index 0000000..9af7d95 --- /dev/null +++ b/charts/libredb-platform/tests/e2e/values-kind.yaml @@ -0,0 +1,20 @@ +# The values of the end-to-end test: the three hosts under .libredb.test, which CoreDNS resolves to Traefik in the +# cluster and the browser maps to 127.0.0.1. The license comes from run-e2e.sh (--set-file platform.license.value). +hosts: + platform: platform.libredb.test + studio: studio.libredb.test + auth: auth.libredb.test +platform: + organizationName: Acme Test +studio: + config: + oidcIssuer: https://auth.libredb.test/realms/libredb + ingress: + hosts: + - host: studio.libredb.test + paths: + - path: / + pathType: Prefix + tls: + - secretName: libredb-tls-studio + hosts: [studio.libredb.test] diff --git a/charts/libredb-platform/tests/e2e/values-openshift.yaml b/charts/libredb-platform/tests/e2e/values-openshift.yaml new file mode 100644 index 0000000..8bf209f --- /dev/null +++ b/charts/libredb-platform/tests/e2e/values-openshift.yaml @@ -0,0 +1,21 @@ +# The values of the OpenShift test (Red Hat Developer Sandbox, one namespace, restricted-v2 SCC): three hosts under +# the cluster's apps domain, which the router serves; OpenShift turns each Ingress into a Route with the Ingress's TLS +# Secret. LIBREDB_OS_DOMAIN in run-openshift.sh replaces apps.example.com. +hosts: + platform: ldb-platform.apps.example.com + studio: ldb-studio.apps.example.com + auth: ldb-auth.apps.example.com +platform: + organizationName: Acme OpenShift +studio: + config: + oidcIssuer: https://ldb-auth.apps.example.com/realms/libredb + ingress: + hosts: + - host: ldb-studio.apps.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: libredb-tls-studio + hosts: [ldb-studio.apps.example.com] diff --git a/charts/libredb-platform/values.yaml b/charts/libredb-platform/values.yaml new file mode 100644 index 0000000..6480cfc --- /dev/null +++ b/charts/libredb-platform/values.yaml @@ -0,0 +1,276 @@ +# LibreDB Platform on Kubernetes. docs/deploy/install-kubernetes.md is the guide; this file documents every value. +# +# The chart is the installer's Kubernetes mode driven by Helm: it renders these values as a LibreDBInstallation +# (installer/src/config/schema.ts, mode kubernetes), and its jobs run the installer's own code on it. One installation +# per namespace: the objects have fixed names, like the installer's Kubernetes constants. + +global: + compatibility: + openshift: + # -- OpenShift's restricted-v2 SCC: "auto" leaves out the pods' fixed runAsUser, runAsGroup and fsGroup when + # the API server is OpenShift, so the SCC assigns ids from the namespace's range; "force" always leaves them + # out, "disabled" never. The Studio chart reads the same value. + adaptSecurityContext: auto + +installation: + # -- metadata.name of the installation: a lowercase DNS label, which names the internal CA. + name: libredb + +# -- The public host of each app. Each needs DNS that the users' browsers and the pods of this release resolve to the +# ingress controller: Platform and Studio call Keycloak's issuer URL from inside the cluster. +hosts: + platform: "" + studio: "" + # -- Keycloak's host, the host of the issuer https:///realms/. With keycloak.mode external it must be + # the host of keycloak.external.baseUrl. + auth: "" + +platform: + # -- The organization's name, which the first install gives the installation's one tenant. + organizationName: "" + # -- Days Platform keeps query events, from 1 to 36500. null keeps Platform's default, 90. + queryLogRetentionDays: null + # -- The license from LibreDB (required): the token, one line that starts with ldl1. Give it inline with + # `--set-file platform.license.value=acme.license`, or name a Secret that holds it. The license names the + # Platform host, so hosts.platform must be that host. + license: + value: "" + existingSecret: "" + key: license + # -- The seal key rotation of installer/docs/runbooks/seal-key-rotation.md, one helm upgrade per step, as + # bridge.sealKeys of the installation: issueNew adds a live key to Studio's keyring, activeKid (16 hex digits) names + # the key Platform seals to (the oldest live key when empty), and retire lists kids to take out of Studio's keyring, + # which an upgrade refuses while a connection still uses one. The secret store keeps every key. + sealKeys: + issueNew: false + activeKid: "" + retire: [] + # -- During a rotation of Platform's AUTH_SECRET: a Secret that holds the previous value, which Platform keeps + # accepting for the sessions it signed, as platform.authSecretPreviousFile of the installation. + authSecretPrevious: + existingSecret: "" + key: auth-secret-previous + image: + repository: ghcr.io/libredb/libredb-platform + # -- The release's tag and digest, from installer/release/release.config.json. + tag: "1.0.1@sha256:b42e346756b4e39a1ed99eee4990092b139dcdc1e0c561069c12ee1bcc081268" + pullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + memory: 1Gi + # -- Support attachments under /app/uploads. Disabled keeps them in an emptyDir, which a restart loses. + persistence: + enabled: true + size: 5Gi + storageClass: "" + accessModes: + - ReadWriteOnce + podAnnotations: {} + podLabels: {} + # -- Platform's pods and the jobs that run its image. The chart adds kubernetes.io/arch: amd64 unless this names an + # arch: the Platform image is built for linux/amd64 alone. + nodeSelector: {} + tolerations: [] + affinity: {} + +tls: + # -- generated: the internal CA issues the ingress certificates of the three hosts, as the installer's + # tls.mode generated does; browsers must trust the CA (Secret libredb-tls-platform holds it as ca.crt). + # secrets: the ingress uses TLS Secrets you provide, such as cert-manager's, named in tls.secrets. + mode: generated + secrets: + platform: "" + studio: "" + auth: "" + # -- With tls.mode secrets, the PEM of the CA that issued those certificates (required there), which every pod + # trusts. A public CA's root works too. + caBundle: "" + +ingress: + # -- The Ingress of Platform and of a managed Keycloak. Studio's Ingress is studio.ingress. + enabled: true + className: "" + annotations: {} + +postgresql: + # -- managed: the chart runs PostgreSQL with verified TLS from the internal CA and provisions it, as the installer + # does. external: your server, provisioned once by a DBA with the script the chart writes to Secret + # libredb-postgres-dba-script. + mode: managed + managed: + image: + repository: docker.io/library/postgres + tag: "18.6@sha256:74935e72241653ca55e0414067e6d8763aceb8a810eb51b452253ec3dcfc4336" + pullPolicy: IfNotPresent + persistence: + size: 10Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + nodeSelector: {} + tolerations: [] + affinity: {} + external: + # -- The server's DNS name, which its certificate covers: Platform and Studio connect with sslmode=verify-full. + host: "" + port: 5432 + # -- The PEM of the CA that issued the server's certificate. + caBundle: "" + +keycloak: + # -- managed: the chart runs Keycloak behind the installer's edge rules and configures its realm. external: your + # Keycloak, configured by the chart's job (provisioning installer) or by your identity team (provisioning manual). + mode: managed + realm: libredb + # -- The installer's first accounts, created with temporary passwords in Secret libredb-keycloak-config. Enabled + # by default with a managed Keycloak only; provisioning manual creates none. + firstAdmin: + enabled: null + username: libredb-admin + userManager: + enabled: null + username: libredb-user-manager + managed: + image: + repository: quay.io/keycloak/keycloak + tag: "26.8.1@sha256:79c623a22abacf3fe6c00a5fb30f5b7843d717b234033b80995891c06116d244" + pullPolicy: IfNotPresent + # -- The installer's edge in front of Keycloak's public paths: only /realms// and /resources/, and no + # non-canonical path, reach Keycloak (installer/src/render/caddyfile.ts). + edge: + image: + repository: docker.io/library/caddy + tag: "2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af" + pullPolicy: IfNotPresent + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 128Mi + resources: + requests: + cpu: 200m + memory: 768Mi + limits: + memory: 1536Mi + nodeSelector: {} + tolerations: [] + affinity: {} + external: + # -- The https base URL of your Keycloak, without /realms. + baseUrl: "" + # -- Where the jobs and Platform reach the Admin API, when not at baseUrl. + adminUrl: "" + # -- installer: the chart's job configures the realm with the admin credential below. manual: your identity + # team configures it from `libredb-installer keycloak export` and gives the two client secrets. + provisioning: installer + adminRealm: master + # -- provisioning installer: a Secret with the admin credential, either a client (adminClientId plus the key + # admin-client-secret) or a user (adminUser plus the key admin-password). + adminSecret: "" + adminClientId: "" + adminUser: "" + # -- provisioning manual: a Secret with the keys studio-client-secret and platform-client-secret. + clientSecretsSecret: "" + # -- The PEM of the CA of your Keycloak's certificate, when a private CA issued it. + caBundle: "" + +network: + # -- An http(s) proxy for the apps' outbound HTTPS, written as HTTPS_PROXY with the installer's NO_PROXY. + httpsProxy: "" + +jobs: + # -- The bootstrap hook, the PostgreSQL provisioning, the Keycloak and the migrate jobs. + backoffLimit: 6 + activeDeadlineSeconds: 900 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 512Mi + +networkPolicy: + # -- PostgreSQL and Keycloak's Admin API reachable only from this release's pods, like the installer's internal + # network; the ingress reaches Keycloak through its edge alone. Takes effect with a CNI that enforces policies. + enabled: true + +imagePullSecrets: [] + +# -- The Studio chart (oci of https://libredb.org/libredb-studio/, pinned by the release manifest) under governance. +# Repeat hosts.studio as the ingress host and the issuer of hosts.auth here: Helm passes no values down to a +# dependency, so the chart refuses to render when they differ. +studio: + enabled: true + fullnameOverride: libredb-studio + image: + repository: ghcr.io/libredb/libredb-studio + tag: "1.0.0@sha256:0cb0177dd2789534f2dbfbc6673104ec0ce70ee82815f5fe9bec5efe3d329ce3" + authProvider: oidc + governance: + enabled: true + postgresSchema: bridge + secrets: + # -- Written by the bootstrap job; keep this name. + existingSecret: libredb-studio-env + adminEmail: "" + userEmail: "" + config: + # -- Must be https:///realms/. + oidcIssuer: "" + oidcRoleClaim: libredb_roles + oidcAdminRoles: admin + oidcUserRoles: user + oidcPrompt: omit + oidcAutoRedirect: true + oidcLogoutHint: true + sessionTtlSeconds: 28800 + authBootstrap: "off" + storageProvider: postgres + storagePostgresSchema: studio + postgresql: + enabled: false + ingress: + enabled: true + className: "" + annotations: {} + # -- Must hold one host, hosts.studio. + hosts: + - host: "" + paths: + - path: / + pathType: Prefix + # -- tls.mode generated: libredb-tls-studio with hosts.studio. + tls: + - secretName: libredb-tls-studio + hosts: [] + extraEnv: + - name: STORAGE_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: libredb-studio-env + key: storage-encryption-key + - name: NODE_EXTRA_CA_CERTS + value: /etc/libredb/ca/ca.crt + # -- The installer's network environment of Studio (NO_PROXY, and HTTPS_PROXY with network.httpsProxy), which the + # bootstrap job writes. + extraEnvFrom: + - secretRef: + name: libredb-studio-network-env + optional: true + extraVolumes: + - name: libredb-ca + configMap: + name: libredb-ca + extraVolumeMounts: + - name: libredb-ca + mountPath: /etc/libredb/ca + readOnly: true diff --git a/docs/deploy/install-docker.md b/docs/deploy/install-docker.md index 1919c9d..562ce95 100644 --- a/docs/deploy/install-docker.md +++ b/docs/deploy/install-docker.md @@ -4,7 +4,7 @@ This guide installs LibreDB 1.0.1 on one x86_64 Linux host with Docker. The installer, `libredb-installer`, runs on that host as root and deploys PostgreSQL, Keycloak, LibreDB Platform, LibreDB Studio and a Caddy edge as the Docker Compose project `libredb`. The guide takes the shortest path: a PostgreSQL and a Keycloak that the installer runs itself, one DNS name per app, and certificates from the installer's internal CA. The [installer reference](../../installer/README.md) covers the other choices: your own certificates or TLS proxy, an existing PostgreSQL server or Keycloak realm, one host without DNS, and a registry mirror. -Kubernetes support comes in the next release. +To install on Kubernetes instead, use the Helm chart, as the [Kubernetes install guide](install-kubernetes.md) describes. ## What you need diff --git a/docs/deploy/install-kubernetes.md b/docs/deploy/install-kubernetes.md new file mode 100644 index 0000000..6b83865 --- /dev/null +++ b/docs/deploy/install-kubernetes.md @@ -0,0 +1,343 @@ +# Install LibreDB on Kubernetes + +This guide installs LibreDB 1.0.1 on a Kubernetes cluster with the Helm chart [`charts/libredb-platform`](../../charts/libredb-platform). +The chart deploys LibreDB Platform, its migrate job and LibreDB Studio under governance, and by default a PostgreSQL server and a Keycloak that it runs itself. +It is the installer's Kubernetes mode driven by Helm: the chart renders its values as an installation configuration of `mode: kubernetes`, and its jobs run the installer's own code on it, so a Helm install gets the same secrets, certificates, environments, PostgreSQL provisioning and Keycloak realm as a [Docker install](install-docker.md) of the same configuration. +The [chart README](../../charts/libredb-platform/README.md) lists every value. + +## What you need + +- Kubernetes 1.28 or newer, with a default StorageClass for the PostgreSQL volume and Platform's uploads. +- An ingress controller with an IngressClass, such as Traefik or HAProxy, which terminates TLS for the three hosts. +- Helm 3. +- At least one x86_64 (amd64) node: the Platform image is built for linux/amd64 alone, so the chart schedules Platform and the jobs that run its image with `kubernetes.io/arch: amd64`; Studio, Keycloak and PostgreSQL also run on arm64 nodes. +- Three DNS names, for Platform, Studio and Keycloak, such as `libredb.acme.example`, `studio.acme.example` and `sso.acme.example`, which resolve to the ingress controller from the users' browsers **and from the cluster's pods**: Platform and Studio call Keycloak's issuer URL `https:///realms/libredb` from inside the cluster. +- A license file from LibreDB for the Platform host, as [Request a license](install-docker.md#request-a-license) describes. + +One namespace holds one installation: the chart's objects have fixed names. + +## Write the values + +```sh +cat > libredb-values.yaml <<'EOF' +hosts: + platform: libredb.acme.example + studio: studio.acme.example + auth: sso.acme.example +platform: + organizationName: Acme Corp +studio: + config: + oidcIssuer: https://sso.acme.example/realms/libredb + ingress: + hosts: + - host: studio.acme.example + paths: + - path: / + pathType: Prefix + tls: + - secretName: libredb-tls-studio + hosts: [studio.acme.example] +EOF +``` + +Helm passes no values down to a dependency, so the Studio host and the issuer appear twice; the chart refuses to render when the copies under `studio:` differ from `hosts`. +Set `ingress.className` and `studio.ingress.className` when the cluster has no default IngressClass. + +## Install + +```sh +helm dependency build charts/libredb-platform +helm install libredb charts/libredb-platform -n libredb --create-namespace \ + -f libredb-values.yaml --set-file platform.license.value=acme.license \ + --wait --wait-for-jobs --timeout 10m +``` + +Instead of `--set-file`, `platform.license.existingSecret` names a Secret that holds the token under the key `license`. +The install runs in this order, and `--wait --wait-for-jobs` returns once every step is done: + +1. The bootstrap hook generates the secrets, the seal keys and the internal CA, and writes every Secret and ConfigMap of the installation. +2. PostgreSQL starts with TLS from the internal CA, and the provisioning job runs the installer's provisioning script, which holds SCRAM verifiers only. +3. Keycloak starts, and the keycloak-configure job converges the realm `libredb`, its clients, roles and the two first accounts, as the installer does. +4. The migrate job migrates Platform's database once the PostgreSQL roles exist, and Platform turns ready. +5. Studio turns ready once Platform's bridge exists. + +On a small cluster the whole install takes about a minute once the images are pulled. + +## Check the license + +```sh +kubectl -n libredb logs deploy/libredb-platform | grep -i licen +``` + +Expected: a line with `"message":"License active"`. +A license that does not name `hosts.platform` starts the install in state `invalid`, as [Check the license](install-docker.md#check-the-license) describes. + +## Trust the internal CA + +With the default `tls.mode: generated`, the internal CA issues the certificates of the three hosts, so every browser that opens them must trust it: + +```sh +kubectl -n libredb get secret libredb-tls-platform -o jsonpath='{.data.ca\.crt}' | base64 -d > libredb-ca.crt +``` + +Import it only where the cluster's operators are trusted: the CA can sign a certificate for any name. +With certificates of your own, such as cert-manager's, set `tls.mode: secrets`, name the TLS Secret of each host in `tls.secrets` (and Studio's again as `studio.ingress.tls[0].secretName`), and give the CA that issued them in `tls.caBundle`, which every pod then trusts; a public CA's root works too. + +## Sign in for the first time + +```sh +kubectl -n libredb get secret libredb-keycloak-config -o jsonpath='{.data.first-admin-password}' | base64 -d; echo +``` + +Open the Platform host, choose Continue with SSO and sign in as `libredb-admin` with that temporary password. +Keycloak then asks for a new password and a TOTP device, as the [Docker guide](install-docker.md#sign-in-for-the-first-time) describes. +`libredb-user-manager` manages the realm's users in Keycloak's admin console, which the ingress does not expose: + +```sh +kubectl -n libredb get secret libredb-keycloak-config -o jsonpath='{.data.user-manager-password}' | base64 -d; echo +kubectl -n libredb port-forward svc/keycloak 18080:8080 +``` + +Then open `http://127.0.0.1:18080/admin/libredb/console/`, the admin URL a Docker install uses too. + +## Back up and restore + +The chart has no scheduled backups, so an installation with a managed PostgreSQL is backed up and restored by hand. +A backup is one set, taken together: the Secrets `libredb-secrets` (the seal keys, which open every database credential Platform stores, the role passwords and the client secrets), `libredb-tls` (the internal CA) and `libredb-installer-state`, and a `pg_dump -Fc` of the databases `libredb` and `keycloak`. +The Secrets must come from the same set as the databases, because the databases hold envelopes sealed to those keys. +Take the first set on day one; the set holds every secret of the installation, so store it like the secrets themselves. +With an external PostgreSQL, the site backs up its server, and the set is the three Secrets. + +```sh +( +set -eu +BACKUP="libredb-backup-$(date -u +%Y%m%dT%H%M%SZ)" +mkdir -m 0700 "$BACKUP" +kubectl -n libredb get secret libredb-secrets libredb-tls libredb-installer-state -o yaml > "$BACKUP/secrets.yaml" +for db in libredb keycloak; do + kubectl -n libredb exec libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -h 127.0.0.1 -U postgres -Fc -d "$1"' sh "$db" > "$BACKUP/$db.dump" +done +echo "$BACKUP" +) +``` + +The bootstrap job's Secrets and ConfigMaps carry no Helm ownership, Platform's upload volume carries `helm.sh/resource-policy: keep`, and PostgreSQL's volume claim outlives its StatefulSet by Kubernetes' own rule, so `helm uninstall` leaves them, and a later install under the same release name in the same namespace reuses the seal keys, the CA, the database and the realm. +Deleting the namespace deletes them for good, and then only a backup set brings the installation back. + +To restore a set into a new namespace or a new cluster: + +1. Create the namespace under the same name: `secrets.yaml` names it, and `kubectl create` refuses it in another one. + Put back the Secrets before the install, so the bootstrap job reuses them instead of generating new ones: + + ```sh + kubectl create namespace libredb + kubectl -n libredb create -f "$BACKUP/secrets.yaml" + ``` + +2. Install the chart with the same values and license as before; it provisions empty databases with the restored role passwords and starts every component. +3. Stop the apps and Keycloak, which leaves PostgreSQL running, and put back each database; the archive recreates every object with its owner and its privileges: + + ```sh + kubectl -n libredb scale deployment libredb-platform libredb-studio keycloak --replicas=0 + kubectl -n libredb wait --for=delete pod -l 'app.kubernetes.io/component in (platform,keycloak)' --timeout=180s + kubectl -n libredb wait --for=delete pod -l app.kubernetes.io/name=studio --timeout=180s + for db in libredb keycloak; do + kubectl -n libredb exec -i libredb-postgres-0 -- sh -c \ + 'PGPASSWORD="$POSTGRES_PASSWORD" pg_restore -h 127.0.0.1 -U postgres --clean --if-exists --exit-on-error -d "$1"' sh "$db" \ + < "$BACKUP/$db.dump" + done + kubectl -n libredb scale deployment libredb-platform libredb-studio keycloak --replicas=1 + ``` + +4. Review what the restore undid: every grant and revocation made after the backup point is gone, so a user whose access was withdrawn since then has it again; withdraw it again in Platform. + +The [Docker runbook](../../installer/docs/runbooks/restore.md#a-database-copy-older-than-a-seal-key-retirement) covers a database copy older than a seal key retirement, which applies here alike. + +## An existing PostgreSQL server + +```yaml +postgresql: + mode: external + external: + host: pg.acme.example + port: 5432 + caBundle: | + -----BEGIN CERTIFICATE----- + ... +``` + +The host must be a DNS name its certificate covers: Platform and Studio connect with `sslmode=verify-full`. +The chart holds no admin credential of the server. +Its bootstrap job writes the provisioning script, with SCRAM verifiers only, to Secret `libredb-postgres-dba-script`, and a DBA runs it once, so install without waiting for the jobs, then let the DBA run the script, then wait: + +```sh +helm install libredb charts/libredb-platform -n libredb --create-namespace -f libredb-values.yaml \ + --set-file platform.license.value=acme.license +kubectl -n libredb get secret libredb-postgres-dba-script -o jsonpath='{.data.provision\.sql}' | base64 -d > libredb-provision.sql +psql "host=pg.acme.example port=5432 dbname=postgres user= sslmode=verify-full sslrootcert=" -f libredb-provision.sql +kubectl -n libredb wait --for=condition=complete job -l app.kubernetes.io/component=platform-migrate --timeout=10m +``` + +The migrate job waits until the roles exist; when the DBA runs the script later than the job's deadline of 15 minutes, `helm upgrade` with the same values runs it again. +The [connection budget](platform-runtime.md#connection-budget) applies to the external server. + +## An existing Keycloak + +```yaml +keycloak: + mode: external + realm: libredb + external: + baseUrl: https://sso.acme.example + provisioning: installer + adminSecret: keycloak-admin + adminClientId: libredb-provisioner +``` + +With `provisioning: installer`, the keycloak-configure job converges the realm's LibreDB objects with the admin credential in the Secret that `adminSecret` names, under the key `admin-client-secret` for a client, or with `adminUser` and the key `admin-password` for a user of `adminRealm` (default `master`). +With `provisioning: manual`, your identity team configures the realm from `libredb-installer keycloak export` or [keycloak-manual.md](../../installer/docs/keycloak-manual.md), and `clientSecretsSecret` names a Secret with the keys `studio-client-secret` and `platform-client-secret`. +`hosts.auth` must be the host of `baseUrl`, and `caBundle` takes the CA of the Keycloak's certificate when a private CA issued it. + +## Upgrade + +```sh +helm upgrade libredb charts/libredb-platform -n libredb -f libredb-values.yaml \ + --set-file platform.license.value=acme.license --wait --wait-for-jobs --timeout 10m +``` + +Every upgrade runs the bootstrap hook, the provisioning, keycloak-configure and migrate jobs again, which converge without changing what is already right. +The apps read their Secrets and ConfigMaps when they start, so the bootstrap hook records each workload whose inputs it changed, and the restart hook, which runs once Helm applied the release, restarts it unless its pods already started after the change: Platform for its environment or the CA bundle, Studio for its Secrets or the CA bundle, Keycloak for its own, and PostgreSQL for a renewed leaf. +So a license renewal, a rotated client secret or a new CA reaches the apps with an upgrade, whether the value comes from these values or from a Secret you provide; nothing watches a Secret you provide between upgrades. +A request to restart outlives a failed run, and the next upgrade carries it out. +A downgrade is not supported: the bootstrap job, which runs before a rollback too, refuses an older release than the installation ran, so restore from a [backup](#back-up-and-restore) instead. +A rollback within the same release, such as one that undoes a wrong value, or the one `helm upgrade --atomic` runs after a failure, runs the bootstrap job with the earlier values; the installation counts a release as its own only once its upgrade succeeded. + +Two Helm behaviours to know when something goes back: + +- `helm rollback` to an older release fails at once on the bootstrap job's refusal, the earlier release keeps running, and Helm leaves the record of that rollback `pending-rollback`, which blocks the next operation. Remove that record, the revision `helm history libredb` shows as `pending-rollback`, with `kubectl -n libredb delete secret sh.helm.release.v1.libredb.v`, then continue with `helm upgrade`. +- When an upgrade fails in its bootstrap hook, nothing was applied, and the rollback of `--atomic` may still fail with `no Job with the name ... found`; the earlier release keeps running, and the next `helm upgrade` goes through. + +## Rotate secrets and the seal key + +Each step below is one `helm upgrade` with your values, as [Upgrade](#upgrade) runs it; keep what a step sets in your values file, because the next upgrade without it would undo it. +Back up Secret `libredb-secrets` before a rotation and keep that copy until the new values work, because a rollback cannot restore a secret value. +`PSQL` below is the managed server's superuser, for the checks: + +```sh +PSQL='kubectl -n libredb exec -i libredb-postgres-0 -- sh -c "PGPASSWORD=\$POSTGRES_PASSWORD psql -X -h 127.0.0.1 -U postgres -d libredb"' +``` + +### The seal key + +Platform seals every database credential to Studio's public seal key and can never open or re-seal one, so the rotation adds a key to Studio first, switches Platform to it, has an admin enter the credentials again, and only then retires the old key, as [the runbook of a Docker install](../../installer/docs/runbooks/seal-key-rotation.md) describes; `platform.sealKeys` stands for its `bridge.sealKeys`. + +1. Note the kid that seals today's credentials, the second dot-separated field of each envelope, and the kids every live Studio instance holds: + + ```sh + echo "SELECT instance_id, keyring_kids, canary FROM bridge.studio_instance WHERE last_seen_at > now() - interval '2 minutes'" | eval "$PSQL" + ``` + +2. Set `platform.sealKeys.issueNew: true` and upgrade. + The bootstrap job issues a new key into the secret store and Studio's keyring, and Studio restarts; Platform keeps sealing to the old key. + Run the query of step 1 until every instance lists the new kid and still reports `ok` for the old one. +3. Set `issueNew: false` and `activeKid: ` in the same edit, and upgrade: Platform restarts with the new public key and publishes a canary for it; wait until every instance reports `ok` for the new kid. + Upgrading with `issueNew: true` again issues no second key while the newest key is the active one. +4. Enter again, in the edit form of each connection, which warns about a credential sealed to an earlier key, every credential the old kid still seals; this query lists them, and must come back empty: + + ```sh + echo "SELECT name, split_part(read_secret, '.', 2) AS read_kid, split_part(write_secret, '.', 2) AS write_kid FROM bridge.connections_v1" | eval "$PSQL" + ``` + + Re-entering a credential closes the affected users' Studio provider sessions, so schedule it outside working hours. +5. Keep `activeKid`, add `retire: []`, and upgrade. + Before it changes anything, the bootstrap job asks PostgreSQL, as `libredb_platform`, which connections the old kid still seals, and while one does the upgrade fails at once (`pre-upgrade hooks failed`), its log naming each connection (`kubectl -n libredb logs job/libredb-bootstrap -c bootstrap`), and nothing changes; enter those credentials and upgrade again. + Otherwise the old key leaves Studio's keyring, the secret store keeps it in escrow as `retired` for a restore of an older backup, and the restart job deletes its canary row before it restarts Studio. + Leave the kid in `retire`. + +### Generated secrets + +The bootstrap job generates every secret of the installation once, into Secret `libredb-secrets`, and never replaces one it holds, so a rotation deletes the value there and upgrades; the upgrade generates a new value and every consumer follows in the same run: the Keycloak client, the PostgreSQL role, the apps' Secrets, and the restart job restarts what reads them. + +```sh +kubectl -n libredb patch secret libredb-secrets --type json -p '[{"op":"remove","path":"/data/"}]' +``` + +| Name | Effect of a new value | +| --- | --- | +| `studio-jwt-secret` | Studio's session cookies: every Studio user is signed out, and signs in again without a password while the Keycloak session lives | +| `platform-auth-secret` | Platform's session cookies: give the previous value as below, so the sessions survive | +| `keycloak-client-libredb-studio`, `keycloak-client-libredb-platform` | The clients' secrets: sign-in fails only between the keycloak-configure job and the app's restart | +| `postgres-owner-password`, `postgres-platform-password`, `postgres-studio-password`, `postgres-keycloak-password` | The roles' passwords, which the provisioning job sets; open connections keep working, a new one with the old password fails until its app restarted | + +Platform keeps its sessions through a rotation of `platform-auth-secret` when the previous value stays for one session lifetime, 8 days: + +```sh +(umask 077 && kubectl -n libredb get secret libredb-secrets -o 'jsonpath={.data.platform-auth-secret}' | base64 -d > auth-previous) +kubectl -n libredb create secret generic libredb-auth-previous --from-file=auth-secret-previous=auth-previous && rm auth-previous +kubectl -n libredb patch secret libredb-secrets --type json -p '[{"op":"remove","path":"/data/platform-auth-secret"}]' +``` + +Upgrade with `platform.authSecretPrevious.existingSecret: libredb-auth-previous`; after 8 days, upgrade without it and delete the Secret. +To end every Platform session at once, skip the previous value. + +`postgres-superuser-password` (the managed server reads it only when it creates its data directory), `postgres-backup-password` and `studio-storage-encryption-key` (Studio's stored data is encrypted with it) are not rotated this way. + +## Differences from a Docker install + +- The ingress controller replaces the installer's Caddy edge for Platform and Studio; a managed Keycloak keeps the installer's edge as a sidecar, so only `/realms//` and `/resources/` reach it. +- The managed Keycloak keeps its bootstrap admin client in realm `master`: Keycloak would recreate it from its Deployment's environment on the next start. Its secret is in Secrets `libredb-secrets`, `libredb-keycloak-env` and `libredb-keycloak-config` and in Keycloak's environment; Keycloak's port 8080, which serves the Admin API in-cluster over plain HTTP, accepts only this release's pods while `networkPolicy.enabled` is true (the default) and the cluster's CNI enforces network policies. +- There is no renewal timer: any `helm upgrade` re-issues a leaf of the internal CA that has fewer than 30 days left and restarts PostgreSQL for its own; the ingress controller picks up a renewed ingress certificate by itself. The leaves are valid for 825 days, so run an upgrade at least once in that time. +- Secrets and the seal key are rotated with values and upgrades, as [Rotate secrets and the seal key](#rotate-secrets-and-the-seal-key) describes, not with the installer's `apply`. +- This release runs one replica of each component. + +## Security notes + +- Every pod runs under the restricted Pod Security Standard, so the namespace may enforce it (`pod-security.kubernetes.io/enforce=restricted`); the managed Keycloak's edge adds only the capability `NET_BIND_SERVICE`, which its binary carries. +- The bootstrap hook's ServiceAccount may create Secrets and ConfigMaps in the namespace, which Kubernetes cannot limit by name, and read and replace only the installation's own objects; it exists only while the hook runs, and a failed hook leaves it until the next run replaces it. +- The containers have memory limits and no CPU limit, so a busy pod is never throttled; set `resources.limits.cpu` of each component where a policy requires one. +- `networkPolicy.enabled` guards PostgreSQL and Keycloak's Admin API; Platform and Studio take traffic from the ingress controller, whose namespace differs per cluster, so a policy for them is yours to add. +- `network.httpsProxy` must hold no credentials: it is part of the installation document, a ConfigMap. +- A license given inline lands in Helm's release Secrets; with GitOps, keep it in a Secret of your own and name it in `platform.license.existingSecret`. + +## Argo CD + +The chart works as an Argo CD application from a Git checkout or a packaged chart: + +- Argo CD runs the bootstrap job as a PreSync hook, the provisioning, keycloak-configure and migrate jobs as Sync hooks, which it replaces on every sync, and the restart job as a PostSync hook, so each sync converges as a `helm upgrade` does. +- The objects the jobs write carry the label `install.libredb.org/installation`, never a tracking label, and the annotations `argocd.argoproj.io/sync-options: Prune=false,Delete=false` and `argocd.argoproj.io/compare-options: IgnoreExtraneous`, so a prune never deletes the seal keys. +- Give the license as `platform.license.existingSecret`. +- Argo CD counts an Ingress healthy only once the ingress controller publishes an address on it, as a cloud load balancer's controller does; a controller behind a NodePort needs its published address set, such as Traefik's `providers.kubernetesIngress.ingressEndpoint.ip`, or the app stays Progressing. + +## OpenShift + +The chart installs on OpenShift 4 with the default `restricted-v2` SCC and namespace-scoped rights only, as in a Red Hat Developer Sandbox: + +- With `global.compatibility.openshift.adaptSecurityContext: auto`, the default, the chart sees `security.openshift.io/v1` and leaves out the pods' fixed `runAsUser`, `runAsGroup` and `fsGroup`, so the SCC assigns them from the namespace's range; the Studio chart reads the same value. `force` does the same where Helm cannot ask the API server, such as `helm template` for GitOps, and `disabled` keeps the fixed ids, for an SCC that allows them. +- OpenShift turns each Ingress into a Route that terminates TLS at the router with the Ingress's Secret, so `tls.mode: generated` works unchanged; the hosts are names under the cluster's apps domain, or names your DNS points at the router. +- The project template may add NetworkPolicies of its own, such as an `allow-same-namespace` policy, and policies add up: there, every pod of the namespace reaches PostgreSQL and Keycloak's Admin API, so give the installation a namespace of its own. + + +`helm dependency build` fetches the pinned Studio chart from `https://libredb.org/libredb-studio/`. +For a site without that access, run `helm package --dependency-update charts/libredb-platform` on a machine that has it and install the archive, which holds the dependency, so Helm on the site needs no repository. +The images come from the registries in `values.yaml`; copy the five of them into your mirror with their digests, for example with `docker buildx imagetools create -t /: ` or `skopeo copy --all`, and point the chart at it, keeping the tags, which pin each image by digest: + +```sh +helm install libredb libredb-platform-.tgz -n libredb -f values.yaml --set-file platform.license.value=acme.license \ + --set platform.image.repository=/libredb/libredb-platform \ + --set postgresql.managed.image.repository=/library/postgres \ + --set keycloak.managed.image.repository=/keycloak/keycloak \ + --set keycloak.managed.edge.image.repository=/library/caddy \ + --set studio.image.repository=/libredb/libredb-studio +``` + +## End-to-end test + +[`charts/libredb-platform/tests/e2e/run-e2e.sh`](../../charts/libredb-platform/tests/e2e/run-e2e.sh) installs the chart on a local kind cluster into a namespace that enforces the restricted Pod Security Standard, checks the endpoints, the network policies, the license and Studio's seal canary, runs the browser suite of [`installer/e2e/kubernetes`](../../installer/e2e/kubernetes), upgrades, uninstalls and installs again; it needs a license for `platform.libredb.test`. +[`run-external.sh`](../../charts/libredb-platform/tests/e2e/run-external.sh) then installs against a PostgreSQL and a Keycloak of a simulated customer, with the customer's TLS Secrets and a license Secret; it needs a license for `platform-ext.libredb.test`. +On the installation run-e2e.sh leaves running (`LIBREDB_E2E_KEEP=1`), [`run-lifecycle.sh`](../../charts/libredb-platform/tests/e2e/run-lifecycle.sh) renews leaves that expire in 10 days and walks a license through expiring, expired and active, [`run-upgrade.sh`](../../charts/libredb-platform/tests/e2e/run-upgrade.sh) upgrades to the chart of a later release, with a failed `--atomic` attempt first, and checks that going back is refused, and [`run-argocd.sh`](../../charts/libredb-platform/tests/e2e/run-argocd.sh) installs Argo CD with label tracking and syncs the packaged chart with automated prune. +`LIBREDB_E2E_EXISTING_CLUSTER=1` runs run-e2e.sh against the cluster of `KUBECONFIG`, such as from a container on kind's network. +[`run-restore.sh`](../../charts/libredb-platform/tests/e2e/run-restore.sh) takes the backup set above, deletes the namespace with its volumes and restores the set, then signs the first admin in with the password of the backup and has a user read a table through a credential sealed before it; [`run-offline.sh`](../../charts/libredb-platform/tests/e2e/run-offline.sh) installs the packaged archive with no Helm repository and no way out, every image from a mirror. +[`run-rotation.sh`](../../charts/libredb-platform/tests/e2e/run-rotation.sh) walks [Rotate secrets and the seal key](#rotate-secrets-and-the-seal-key) with a granted user querying throughout; [`run-resilience.sh`](../../charts/libredb-platform/tests/e2e/run-resilience.sh) kills PostgreSQL, then Platform, Studio and Keycloak at once, then restarts the node, and checks after each that the endpoints come back on their own and no data was lost; [`run-soak.sh`](../../charts/libredb-platform/tests/e2e/run-soak.sh) leaves one installation running for hours and records availability, memory, restarts and growth. +[`run-openshift.sh`](../../charts/libredb-platform/tests/e2e/run-openshift.sh) runs the same checks and the browser suite in the namespace of the current `oc login`, with the hosts `ldb--.`, and checks that `restricted-v2` admits every pod with a uid from the namespace's range. diff --git a/docs/deploy/platform-runtime.md b/docs/deploy/platform-runtime.md index fd8c4bc..d890e90 100644 --- a/docs/deploy/platform-runtime.md +++ b/docs/deploy/platform-runtime.md @@ -62,7 +62,7 @@ A self-hosted install leaves these variables unset: ## Migrate container -The `migrate` command runs as a one-shot container (a Docker Compose service that exits, or a Kubernetes Job once Kubernetes support arrives in the next release) before every start of a new image. +The `migrate` command runs as a one-shot container (a Docker Compose service that exits, or a Kubernetes Job of the Helm chart, [install-kubernetes.md](install-kubernetes.md)) before every start of a new image. It receives exactly these variables, and the serve container never receives `MIGRATE_DATABASE_URL`: `serve` refuses to start when it is set. | Variable | Used by | Value | diff --git a/installer/README.md b/installer/README.md index 0297d99..81a7731 100644 --- a/installer/README.md +++ b/installer/README.md @@ -67,7 +67,7 @@ The Docker Engine minimum and that refused range keep the ports published to `12 The refused range lives in the preflight module instead of the release manifest, because a minimum cannot exclude it and still admit 28.0.x and 28.1.x, and its message names the advisory GHSA-x4rx-4gw3-53p4 and asks for 28.3.3 or newer, which fixes it. The [Docker install guide](../docs/deploy/install-docker.md) lists every prerequisite of the host. Run every command below as root. -This build applies only `mode: docker`: `preflight`, `plan`, `apply`, `verify` and `db-script` refuse any other mode with exit 2, and Kubernetes mode follows in the next release. +This build applies only `mode: docker`: `preflight`, `plan`, `apply`, `verify` and `db-script` refuse any other mode with exit 2. On Kubernetes, the Helm chart `charts/libredb-platform` runs the installer's Kubernetes-mode logic in its jobs (`src/kubernetes/`, bundled by `bun run build:helm-jobs`), as [install-kubernetes.md](../docs/deploy/install-kubernetes.md) describes. This build applies every `tls.mode` (`generated`, `provided` and `external`) and `postgres.mode` `managed` or `external`. Keycloak is either managed, or the customer's own Keycloak with a realm the installer reuses (see External Keycloak below). diff --git a/installer/e2e/helpers/studio.ts b/installer/e2e/helpers/studio.ts index 351c11a..7c25c83 100644 --- a/installer/e2e/helpers/studio.ts +++ b/installer/e2e/helpers/studio.ts @@ -13,8 +13,8 @@ import { type PasswordUser, } from "./platform"; -/** Studio's URL in test/fixtures/configs/docker.yaml. */ -export const STUDIO_URL = "https://studio.libredb.test"; +/** Studio's URL in test/fixtures/configs/docker.yaml; LIBREDB_E2E_STUDIO_URL points the Helm chart's suite elsewhere. */ +export const STUDIO_URL = process.env.LIBREDB_E2E_STUDIO_URL ?? "https://studio.libredb.test"; /** The Studio container of the Compose project libredb. */ export const STUDIO_CONTAINER = "libredb-studio-1"; diff --git a/installer/e2e/kubernetes/helm-install.k8s.ts b/installer/e2e/kubernetes/helm-install.k8s.ts new file mode 100644 index 0000000..484cd09 --- /dev/null +++ b/installer/e2e/kubernetes/helm-install.k8s.ts @@ -0,0 +1,211 @@ +// The browser suite of a Helm install: on the installation run-e2e.sh made on kind, the first admin signs in for the +// first time (new password and TOTP), sees the license, connects a target PostgreSQL and grants a Keycloak user read +// access; the user reads the table in Studio and cannot write to it; the grant's revocation reaches Studio within +// seconds; and Platform's audit log shows the user's statement. Platform and Studio never call each other, so every +// effect crossed the bridge schema of the shared PostgreSQL. The UI steps are the installer's own browser helpers +// (installer/e2e/helpers), so a Helm install is held to what a Docker install is held to. Each step saves a +// screenshot under LIBREDB_E2E_SCREENSHOTS. +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test, type BrowserContext, type Page } from "@playwright/test"; +import { + createConnection, + deleteConnection, + FIRST_ADMIN, + firstLoginToPlatform, + grantAccess, + grantRemoved, + openConnection, + revokeGrant, + signedInFirstAdmin, + signInFirstAdminToPlatform, + type FirstLogin, +} from "../helpers/platform"; +import { openStudioConsole, signInToStudioAs, STUDIO_URL, studioRequest, type StudioAnswer } from "../helpers/studio"; +import { + createKeycloakUser, + deleteKeycloakUser, + kubectl, + PLATFORM_URL, + psqlAsSuperuser, + removeTargetDatabase, + secretValue, + startTargetDatabase, + stopKeycloakForward, + targetRowCount, + type KeycloakUser, + type TargetDatabase, +} from "./k8s"; + +test.describe.configure({ mode: "serial" }); + +const SHOTS = process.env.LIBREDB_E2E_SCREENSHOTS ?? join("test-results", "helm-screenshots"); +const REVOCATION_LIMIT_MS = 15_000; + +const firstAdmin: FirstLogin = { + username: FIRST_ADMIN, + temporaryPassword: secretValue("libredb-keycloak-config", "first-admin-password"), + newPassword: `K8s-${Date.now()}-Admin-Pass!`, + email: "admin@e2e.libredb.test", + firstName: "Helm", + lastName: "Admin", +}; + +let target: TargetDatabase; +let user: KeycloakUser; +let adminContext: BrowserContext; +let userContext: BrowserContext; +let platform: Page; +let studio: Page; +let connection: { id: string; name: string } | undefined; + +async function shot(page: Page, name: string): Promise { + mkdirSync(SHOTS, { recursive: true }); + await page.screenshot({ path: join(SHOTS, `${name}.png`), fullPage: true }); +} + +function studioId(): string { + if (connection === undefined) throw new Error("no connection yet"); + return `seed:${connection.id}`; +} + +function query(page: Page, sql: string): Promise { + return studioRequest(page, "/api/db/query", { body: { connectionId: studioId(), sql } }); +} + +async function managedIds(page: Page): Promise { + const answer = await studioRequest(page, "/api/connections/managed"); + expect(answer.status, JSON.stringify(answer.body)).toBe(200); + return (answer.body as { connections: { id: string }[] }).connections.map((managed) => managed.id); +} + +test.beforeAll(async ({ browser }) => { + const image = kubectl(["get", "statefulset", "libredb-postgres", "-o", "jsonpath={.spec.template.spec.containers[0].image}"]); + target = startTargetDatabase(image); + user = await createKeycloakUser(`analyst-${Date.now().toString(36)}`, "user"); + adminContext = await browser.newContext(); + userContext = await browser.newContext(); + platform = await adminContext.newPage(); + studio = await userContext.newPage(); +}); + +test.afterAll(async () => { + if (connection !== undefined) { + try { + // The admin's session from the first test is still open. + await deleteConnection(platform, connection); + } catch (error) { + console.log(`cleanup: the connection could not be deleted: ${String(error)}`); + } + } + if (user !== undefined) await deleteKeycloakUser(user.id); + removeTargetDatabase(); + stopKeycloakForward(); + await adminContext?.close(); + await userContext?.close(); +}); + +test("the first admin signs in to Platform for the first time with a new password and TOTP", async () => { + // A rerun against the same installation signs in with the password and TOTP device the first run set. + if (signedInFirstAdmin() === undefined) { + const { completed } = await firstLoginToPlatform(platform, firstAdmin); + expect(completed).toContain("password"); + expect(completed).toContain("totp"); + } else { + await signInFirstAdminToPlatform(platform, firstAdmin); + } + await expect(platform).toHaveURL((url) => url.origin === PLATFORM_URL && url.pathname !== "/login"); + await platform.waitForLoadState("networkidle"); + await shot(platform, "01-platform-after-first-login"); +}); + +test("the license page shows the license the chart installed as active", async () => { + await platform.goto("/admin/license"); + // The license id the script read from the license file it installed. + const licenseId = process.env.LIBREDB_E2E_LICENSE_ID; + if (licenseId !== undefined && licenseId !== "") await expect(platform.locator("body")).toContainText(licenseId); + await expect(platform.locator("body")).toContainText(/active/i); + await shot(platform, "02-platform-license"); +}); + +test("the admin connects the target PostgreSQL, sealing its credentials to Studio's key", async () => { + const name = `orders-${Date.now().toString(36)}`; + await createConnection(platform, { + name, + host: target.host, + port: target.port, + database: target.database, + read: target.read, + write: target.write, + }); + const id = await openConnection(platform, name); + connection = { id, name }; + // Platform stores only lsb1 envelopes: neither password is in its tables. + const stored = psqlAsSuperuser("libredb", `SELECT "readSecret" || ' ' || coalesce("writeSecret", '') FROM platform.database_connections WHERE id = '${id}'`); + expect(stored).toMatch(/^lsb1\./); + expect(stored).not.toContain(target.read.password); + expect(stored).not.toContain(target.write.password); + await shot(platform, "03-platform-connection"); +}); + +test("the admin grants a Keycloak user read access", async () => { + await grantAccess(platform, user.username, "read"); + await shot(platform, "04-platform-grant-read"); +}); + +test("the user signs in to Studio, sees the connection and reads the table, but cannot write", async () => { + await signInToStudioAs(studio, user); + await openStudioConsole(studio); + await expect.poll(() => managedIds(studio), { timeout: 30_000 }).toContain(studioId()); + const read = await query(studio, `SELECT item, qty FROM ${target.table} ORDER BY id`); + expect(read.status, JSON.stringify(read.body)).toBe(200); + expect(JSON.stringify(read.body)).toContain("keyboard"); + const before = targetRowCount(); + const write = await query(studio, `INSERT INTO ${target.table} (item, qty) VALUES ('blocked', 1)`); + expect(write.status, JSON.stringify(write.body)).not.toBe(200); + expect(targetRowCount()).toBe(before); + await studio.goto(STUDIO_URL); + await studio.waitForLoadState("networkidle"); + await expect(studio.locator("body")).toContainText(connection?.name ?? ""); + await shot(studio, "05-studio-user-sees-connection"); + // The same read through Studio's editor, as the user runs it. + const editor = studio.locator(".monaco-editor").first(); + await editor.click(); + await studio.keyboard.type(`SELECT item, qty FROM ${target.table} ORDER BY id`); + await studio.getByRole("button", { name: "RUN", exact: true }).click(); + await expect(studio.locator("body")).toContainText("monitor", { timeout: 30_000 }); + await shot(studio, "05b-studio-user-query-result"); +}); + +test("revoking the grant in Platform takes the connection away from Studio within seconds", async () => { + await openConnection(platform, connection?.name ?? ""); + const { answeredAt } = await revokeGrant(platform, user.username); + await grantRemoved(platform, user.username); + await openStudioConsole(studio); + let latency = -1; + for (;;) { + const answer = await query(studio, "SELECT 1"); + if (answer.status === 404) { + latency = Date.now() - answeredAt; + break; + } + if (Date.now() - answeredAt > REVOCATION_LIMIT_MS) throw new Error(`Studio still answered ${answer.status} after the revocation`); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + console.log(`revocation latency: ${latency} ms`); + test.info().annotations.push({ type: "revocation latency", description: `${latency} ms` }); + await shot(platform, "06-platform-grant-revoked"); +}); + +test("Platform's audit log shows the user's statement", async () => { + await expect + .poll( + async () => { + await platform.goto("/audit"); + return platform.locator("body").innerText(); + }, + { timeout: 60_000 }, + ) + .toContain(user.username); + await shot(platform, "07-platform-audit-log"); +}); diff --git a/installer/e2e/kubernetes/k8s.ts b/installer/e2e/kubernetes/k8s.ts new file mode 100644 index 0000000..6683219 --- /dev/null +++ b/installer/e2e/kubernetes/k8s.ts @@ -0,0 +1,232 @@ +// The Kubernetes side of the browser specs: kubectl against the kind cluster, the target database as a pod in the +// installation's namespace, and Keycloak users created through the Admin API with the managed Keycloak's bootstrap +// admin client, which a port-forward to Service keycloak reaches. The client secret is read from Secret +// libredb-keycloak-config and stays in this process. +import { execFileSync, spawn, type ChildProcess } from "node:child_process"; +import { randomBytes } from "node:crypto"; + +export const NAMESPACE = process.env.LIBREDB_E2E_NAMESPACE ?? "libredb"; +const KUBECTL = process.env.KUBECTL ?? "kubectl"; +const REALM = "libredb"; +export const PLATFORM_URL = process.env.LIBREDB_E2E_PLATFORM_URL ?? "https://platform.libredb.test"; +// LIBREDB_E2E_OPENSHIFT=1: the namespace's SCC assigns the pods' uids, and rejects a fixed one outside its range. +const FIXED_UIDS = process.env.LIBREDB_E2E_OPENSHIFT === "1" ? {} : { runAsUser: 999, runAsGroup: 999, fsGroup: 999 }; + +export function kubectl(args: string[], input?: string): string { + return execFileSync(KUBECTL, ["-n", NAMESPACE, ...args], { + encoding: "utf8", + input, + stdio: ["pipe", "pipe", "pipe"], + }).trim(); +} + +export function secretValue(name: string, key: string): string { + const encoded = kubectl(["get", "secret", name, "-o", `jsonpath={.data.${key.replaceAll(".", "\\.")}}`]); + return Buffer.from(encoded, "base64").toString("utf8"); +} + +/** psql as the managed server's superuser in the pod libredb-postgres-0, with unaligned tuples-only output. */ +export function psqlAsSuperuser(database: string, sql: string): string { + return kubectl([ + "exec", + "libredb-postgres-0", + "--", + "sh", + "-c", + `PGPASSWORD="$POSTGRES_PASSWORD" psql -X -q -A -t -v ON_ERROR_STOP=1 -h 127.0.0.1 -U postgres -d ${database} -c "$0"`, + sql, + ]); +} + +function secret(): string { + return randomBytes(18).toString("base64url"); +} + +export interface Credential { + user: string; + password: string; +} + +export interface TargetDatabase { + host: string; + port: number; + database: string; + table: string; + read: Credential; + write: Credential; +} + +const TARGET = "e2e-target"; +// LIBREDB_E2E_TARGET_NAMESPACE keeps the target database out of the installation's namespace, as a customer's own +// database is, so a test can delete that namespace and restore the installation from a backup. +const TARGET_NAMESPACE = process.env.LIBREDB_E2E_TARGET_NAMESPACE ?? NAMESPACE; + +/** kubectl in the target database's namespace: a later -n wins over kubectl()'s. */ +function targetKubectl(args: string[], input?: string): string { + return kubectl(["-n", TARGET_NAMESPACE, ...args], input); +} + +/** Starts a PostgreSQL pod and Service e2e-target with a read role and a write role on one table. */ +export function startTargetDatabase(image: string): TargetDatabase { + removeTargetDatabase(); + const superuser = secret(); + targetKubectl( + ["apply", "-f", "-"], + JSON.stringify({ + apiVersion: "v1", + kind: "List", + items: [ + { + apiVersion: "v1", + kind: "Pod", + metadata: { name: TARGET, labels: { app: TARGET } }, + // Restricted, as the installation's namespace enforces. + spec: { + securityContext: { runAsNonRoot: true, ...FIXED_UIDS, seccompProfile: { type: "RuntimeDefault" } }, + containers: [ + { + name: "postgres", + image, + env: [ + { name: "POSTGRES_PASSWORD", value: superuser }, + { name: "POSTGRES_DB", value: "interop" }, + ], + securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } }, + readinessProbe: { exec: { command: ["pg_isready", "-h", "127.0.0.1", "-U", "postgres"] }, periodSeconds: 2 }, + volumeMounts: [ + { name: "data", mountPath: "/var/lib/postgresql" }, + { name: "run", mountPath: "/var/run/postgresql" }, + ], + }, + ], + volumes: [ + { name: "data", emptyDir: {} }, + { name: "run", emptyDir: {} }, + ], + }, + }, + { + apiVersion: "v1", + kind: "Service", + metadata: { name: TARGET }, + spec: { selector: { app: TARGET }, ports: [{ port: 5432 }] }, + }, + ], + }), + ); + targetKubectl(["wait", "--for=condition=Ready", `pod/${TARGET}`, "--timeout=180s"]); + const read = { user: "e2e_reader", password: secret() }; + const write = { user: "e2e_writer", password: secret() }; + const sql = [ + "CREATE SCHEMA shop;", + "CREATE TABLE shop.orders (id serial PRIMARY KEY, item text NOT NULL, qty int NOT NULL);", + "INSERT INTO shop.orders (item, qty) VALUES ('keyboard', 2), ('monitor', 1), ('cable', 5);", + `CREATE ROLE ${read.user} LOGIN PASSWORD '${read.password}';`, + `CREATE ROLE ${write.user} LOGIN PASSWORD '${write.password}';`, + `GRANT USAGE ON SCHEMA shop TO ${read.user}, ${write.user};`, + `GRANT SELECT ON shop.orders TO ${read.user};`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON shop.orders TO ${write.user};`, + `GRANT USAGE ON SEQUENCE shop.orders_id_seq TO ${write.user};`, + ].join("\n"); + targetKubectl(["exec", "-i", TARGET, "--", "psql", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-U", "postgres", "-d", "interop"], sql); + return { host: `${TARGET}.${TARGET_NAMESPACE}.svc.cluster.local`, port: 5432, database: "interop", table: "shop.orders", read, write }; +} + +export function removeTargetDatabase(): void { + try { + targetKubectl(["delete", "pod,service", TARGET, "--ignore-not-found", "--wait=true"]); + } catch { + // Nothing to remove. + } +} + +export function targetRowCount(): number { + return Number(targetKubectl(["exec", TARGET, "--", "psql", "-X", "-A", "-t", "-U", "postgres", "-d", "interop", "-c", "SELECT count(*) FROM shop.orders"])); +} + +let forward: ChildProcess | undefined; +// Not 18080, the admin port of a Docker install, which a host may already use. +const FORWARD_PORT = Number(process.env.LIBREDB_E2E_KEYCLOAK_PORT ?? "28080"); + +/** A port-forward to Service keycloak on 127.0.0.1, which reaches the Admin API of a managed Keycloak. */ +async function keycloakAdminUrl(): Promise { + if (forward === undefined) { + forward = spawn(KUBECTL, ["-n", NAMESPACE, "port-forward", "svc/keycloak", `${FORWARD_PORT}:8080`], { stdio: "ignore" }); + for (let attempt = 0; attempt < 50; attempt += 1) { + try { + await fetch(`http://127.0.0.1:${FORWARD_PORT}/realms/${REALM}`); + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + } + return `http://127.0.0.1:${FORWARD_PORT}`; +} + +export function stopKeycloakForward(): void { + forward?.kill(); + forward = undefined; +} + +async function adminToken(base: string): Promise { + const response = await fetch(`${base}/realms/master/protocol/openid-connect/token`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: "libredb-bootstrap", + client_secret: secretValue("libredb-keycloak-config", "keycloak-bootstrap-client-secret"), + }), + }); + if (!response.ok) throw new Error(`Keycloak refused the bootstrap client: ${response.status}`); + return ((await response.json()) as { access_token: string }).access_token; +} + +export interface KeycloakUser { + id: string; + username: string; + email: string; + password: string; +} + +/** Creates a user of the realm with a password and the client role libredb:, as the user manager would. */ +export async function createKeycloakUser(username: string, role: "admin" | "user"): Promise { + const base = await keycloakAdminUrl(); + const token = await adminToken(base); + const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" }; + const realm = `${base}/admin/realms/${REALM}`; + const password = `${secret()}Aa1!`; + const email = `${username}@e2e.libredb.test`; + const created = await fetch(`${realm}/users`, { + method: "POST", + headers, + body: JSON.stringify({ + username, + email, + emailVerified: true, + firstName: username, + lastName: "E2E", + enabled: true, + credentials: [{ type: "password", value: password, temporary: false }], + }), + }); + if (created.status !== 201) throw new Error(`creating ${username} answered ${created.status}: ${await created.text()}`); + const id = (created.headers.get("location") ?? "").split("/").pop() as string; + const clients = (await (await fetch(`${realm}/clients?clientId=libredb`, { headers })).json()) as { id: string }[]; + const client = clients[0] as { id: string }; + const roleRep = await (await fetch(`${realm}/clients/${client.id}/roles/${role}`, { headers })).json(); + const mapped = await fetch(`${realm}/users/${id}/role-mappings/clients/${client.id}`, { + method: "POST", + headers, + body: JSON.stringify([roleRep]), + }); + if (mapped.status !== 204) throw new Error(`granting libredb:${role} to ${username} answered ${mapped.status}`); + return { id, username, email, password }; +} + +export async function deleteKeycloakUser(id: string): Promise { + const base = await keycloakAdminUrl(); + const token = await adminToken(base); + await fetch(`${base}/admin/realms/${REALM}/users/${id}`, { method: "DELETE", headers: { authorization: `Bearer ${token}` } }); +} diff --git a/installer/e2e/kubernetes/playwright.config.ts b/installer/e2e/kubernetes/playwright.config.ts new file mode 100644 index 0000000..3acd2a4 --- /dev/null +++ b/installer/e2e/kubernetes/playwright.config.ts @@ -0,0 +1,30 @@ +// The browser specs of the Helm chart, run against an installation on a kind cluster that run-e2e.sh made with the +// hostnames *.libredb.test, which Traefik serves on 127.0.0.1:443: +// cd installer && bunx playwright test --config e2e/kubernetes/playwright.config.ts +// The browser maps every *.libredb.test name to 127.0.0.1 and trusts nothing it is not told to: the ingress serves +// leaves of the installation's internal CA, which run-e2e.sh checks with curl against the CA, so the browser accepts +// them as the installer's own browser specs do. +// On a cluster whose hosts resolve publicly, such as OpenShift's apps domain, LIBREDB_E2E_PLATFORM_URL and +// LIBREDB_E2E_STUDIO_URL name the two apps and the browser resolves them as it would anywhere. +import { defineConfig, devices } from "@playwright/test"; +import { PLATFORM_URL } from "./k8s"; + +export default defineConfig({ + testDir: ".", + // Not *.spec.ts, so the Docker suite of e2e/playwright.config.ts never picks this one up. + testMatch: "*.k8s.ts", + workers: 1, + fullyParallel: false, + retries: 0, + timeout: 4 * 60_000, + reporter: [["list"]], + outputDir: process.env.LIBREDB_E2E_OUTPUT_DIR ?? "test-results", + use: { + ...devices["Desktop Chrome"], + baseURL: PLATFORM_URL, + ignoreHTTPSErrors: true, + trace: "retain-on-failure", + screenshot: "only-on-failure", + launchOptions: { args: process.env.LIBREDB_E2E_PLATFORM_URL ? [] : ["--host-resolver-rules=MAP *.libredb.test 127.0.0.1"] }, + }, +}); diff --git a/installer/e2e/kubernetes/restore.k8s.ts b/installer/e2e/kubernetes/restore.k8s.ts new file mode 100644 index 0000000..1bdf953 --- /dev/null +++ b/installer/e2e/kubernetes/restore.k8s.ts @@ -0,0 +1,116 @@ +// The browser side of run-restore.sh and run-resilience.sh, in two phases that LIBREDB_E2E_RESTORE_PHASE picks, around +// what the script does to the installation in between: a backup, the loss of the namespace and a restore, or killed +// pods and a restarted node. seed: the first admin signs in for the first time, connects the target PostgreSQL, which +// lives in a namespace of its own, and grants a Keycloak user read access, and the user reads the table in Studio; the +// phase writes what it made to LIBREDB_E2E_RESTORE_STATE. verify: the first admin signs in with the password and TOTP +// device of the seed, Platform still holds the connection with the envelope of the seed, and the user still reads the +// table in Studio, so Studio opened the credential sealed in the seed. Without LIBREDB_E2E_RESTORE_PHASE, as in +// run-e2e.sh, the suite skips. +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test, type BrowserContext, type Page } from "@playwright/test"; +import { createConnection, FIRST_ADMIN, grantAccess, openConnection, signInFirstAdminToPlatform, type FirstLogin } from "../helpers/platform"; +import { openStudioConsole, signInToStudioAs, STUDIO_URL, studioRequest } from "../helpers/studio"; +import { createKeycloakUser, kubectl, psqlAsSuperuser, secretValue, startTargetDatabase, stopKeycloakForward, type KeycloakUser, type TargetDatabase } from "./k8s"; + +const PHASE = process.env.LIBREDB_E2E_RESTORE_PHASE; +const STATE = process.env.LIBREDB_E2E_RESTORE_STATE ?? join("test-results", "restore-state.json"); +const SHOTS = process.env.LIBREDB_E2E_SCREENSHOTS ?? join("test-results", "restore-screenshots"); + +interface RestoreState { + target: TargetDatabase; + user: KeycloakUser; + connection: { id: string; name: string }; + envelope: string; +} + +test.describe.configure({ mode: "serial" }); +test.skip(PHASE !== "seed" && PHASE !== "verify", "run by run-restore.sh and run-resilience.sh only"); + +let adminContext: BrowserContext; +let userContext: BrowserContext; +let platform: Page; +let studio: Page; + +async function shot(page: Page, name: string): Promise { + mkdirSync(SHOTS, { recursive: true }); + await page.screenshot({ path: join(SHOTS, `${name}.png`), fullPage: true }); +} + +function firstAdmin(): FirstLogin { + return { + username: FIRST_ADMIN, + temporaryPassword: secretValue("libredb-keycloak-config", "first-admin-password"), + newPassword: `Restore-${Date.now()}-Admin-Pass!`, + email: "admin@e2e.libredb.test", + firstName: "Restore", + lastName: "Admin", + }; +} + +function envelopeOf(id: string): string { + return psqlAsSuperuser("libredb", `SELECT "readSecret" FROM platform.database_connections WHERE id = '${id}'`); +} + +async function userReadsTable(state: RestoreState): Promise { + await signInToStudioAs(studio, state.user); + await openStudioConsole(studio); + const connectionId = `seed:${state.connection.id}`; + await expect + .poll(async () => { + const answer = await studioRequest(studio, "/api/connections/managed"); + return answer.status === 200 ? (answer.body as { connections: { id: string }[] }).connections.map((managed) => managed.id) : []; + }, { timeout: 60_000 }) + .toContain(connectionId); + const read = await studioRequest(studio, "/api/db/query", { + body: { connectionId, sql: `SELECT item, qty FROM ${state.target.table} ORDER BY id` }, + }); + expect(read.status, JSON.stringify(read.body)).toBe(200); + expect(JSON.stringify(read.body)).toContain("keyboard"); +} + +test.beforeAll(async ({ browser }) => { + adminContext = await browser.newContext(); + userContext = await browser.newContext(); + platform = await adminContext.newPage(); + studio = await userContext.newPage(); +}); + +test.afterAll(async () => { + stopKeycloakForward(); + await adminContext?.close(); + await userContext?.close(); +}); + +test("seed: the first admin connects the target database and grants a user, who reads the table in Studio", async () => { + test.skip(PHASE !== "seed"); + if (existsSync(STATE)) throw new Error(`${STATE} exists: the seed phase runs once per backup`); + const image = kubectl(["get", "statefulset", "libredb-postgres", "-o", "jsonpath={.spec.template.spec.containers[0].image}"]); + const target = startTargetDatabase(image); + const user = await createKeycloakUser(`restored-${Date.now().toString(36)}`, "user"); + await signInFirstAdminToPlatform(platform, firstAdmin()); + const name = `restore-${Date.now().toString(36)}`; + await createConnection(platform, { name, host: target.host, port: target.port, database: target.database, read: target.read, write: target.write }); + const id = await openConnection(platform, name); + await grantAccess(platform, user.username, "read"); + const state: RestoreState = { target, user, connection: { id, name }, envelope: envelopeOf(id) }; + expect(state.envelope).toMatch(/^lsb1\./); + await userReadsTable(state); + writeFileSync(STATE, JSON.stringify(state), { mode: 0o600 }); + await shot(platform, "seed-platform-connection"); +}); + +test("verify: the installation signs the first admin in and Studio opens the credential sealed in the seed", async () => { + test.skip(PHASE !== "verify"); + const state = JSON.parse(readFileSync(STATE, "utf8")) as RestoreState; + // The envelope is the backup's, byte for byte: the restore did not seal it again. + expect(envelopeOf(state.connection.id)).toBe(state.envelope); + await signInFirstAdminToPlatform(platform, firstAdmin()); + expect(await openConnection(platform, state.connection.name)).toBe(state.connection.id); + await shot(platform, "verify-platform-connection"); + await userReadsTable(state); + await studio.goto(STUDIO_URL); + await studio.waitForLoadState("networkidle"); + await expect(studio.locator("body")).toContainText(state.connection.name); + await shot(studio, "verify-studio-connection"); +}); diff --git a/installer/e2e/kubernetes/rotation.k8s.ts b/installer/e2e/kubernetes/rotation.k8s.ts new file mode 100644 index 0000000..8f2cc5c --- /dev/null +++ b/installer/e2e/kubernetes/rotation.k8s.ts @@ -0,0 +1,303 @@ +// The rotations of "Rotate secrets and the seal key" in docs/deploy/install-kubernetes.md, on the installation +// run-rotation.sh made on kind, each step a helm upgrade as the guide gives it. The seal key drill follows the runbook +// of a Docker install (installer/e2e/runbooks.spec.ts): a new key reaches every Studio heartbeat, Platform switches to +// it, a retirement while a credential still uses the old key is refused at once and changes nothing, the admin enters +// the credentials again, and the retirement then takes the old key out of Studio's keyring and its canary out of +// Platform's table, while the granted user keeps querying. Then Studio's JWT_SECRET, Platform's AUTH_SECRET with its +// previous value, Studio's Keycloak client secret and libredb_platform's password are rotated by deleting each from +// Secret libredb-secrets and upgrading. Without LIBREDB_E2E_ROTATION=1, as in run-e2e.sh, the suite skips. +import { execFileSync, spawnSync } from "node:child_process"; +import { expect, test, type BrowserContext, type Page } from "@playwright/test"; +import { sealKeyId } from "../../src/secrets/seal-keys"; +import { + createConnection, + FIRST_ADMIN, + grantAccess, + openConnection, + signInFirstAdminToPlatform, + type Credential, +} from "../helpers/platform"; +import { openStudioConsole, signInToStudioAs, STUDIO_URL, studioRequest, type StudioAnswer } from "../helpers/studio"; +import { + createKeycloakUser, + kubectl, + NAMESPACE, + PLATFORM_URL, + psqlAsSuperuser, + secretValue, + startTargetDatabase, + stopKeycloakForward, + type KeycloakUser, + type TargetDatabase, +} from "./k8s"; + +const ENABLED = process.env.LIBREDB_E2E_ROTATION === "1"; +const CHART = process.env.LIBREDB_E2E_CHART ?? ""; +const VALUES = process.env.LIBREDB_E2E_VALUES ?? ""; +const LICENSE = process.env.LIBREDB_E2E_LICENSE ?? ""; +const HEARTBEAT_WAIT_MS = 180_000; + +test.describe.configure({ mode: "serial" }); +test.skip(!ENABLED, "run by run-rotation.sh only"); + +let target: TargetDatabase; +let user: KeycloakUser; +let adminContext: BrowserContext; +let userContext: BrowserContext; +let platform: Page; +let studio: Page; +let connection: { id: string; name: string }; +/** The --set flags of platform.sealKeys the installation runs with, which every later upgrade repeats. */ +let sealSets: string[] = []; + +/** helm upgrade of the installation with the values of run-rotation.sh and `sets`; returns helm's exit status. */ +function upgrade(sets: string[]): { status: number; output: string } { + const args = ["upgrade", "libredb", CHART, "-n", NAMESPACE, "-f", VALUES, "--set-file", `platform.license.value=${LICENSE}`]; + for (const set of [...sealSets, ...sets]) args.push("--set", set); + const run = spawnSync("helm", [...args, "--wait", "--wait-for-jobs", "--timeout", "10m"], { encoding: "utf8" }); + return { status: run.status ?? -1, output: `${run.stdout}${run.stderr}` }; +} + +/** The pods of a selector, terminating ones included. */ +function podCount(selector: string): number { + return kubectl(["get", "pods", "-l", selector, "-o", "name"]).split("\n").filter((line) => line !== "").length; +} + +/** Whether the ingress answers `url` within 5 seconds with `status`, three times in a row. */ +function settled(url: string, status: string): boolean { + const host = new URL(url).host; + for (let attempt = 0; attempt < 3; attempt += 1) { + const run = spawnSync("curl", ["-sk", "--max-time", "5", "--resolve", `${host}:443:127.0.0.1`, "-o", "/dev/null", "-w", "%{http_code}", url], { encoding: "utf8" }); + if (run.stdout !== status) return false; + } + return true; +} + +/** + * An upgrade that succeeds, once the restarted apps run, their old pods are gone and the ingress stopped sending + * requests to them. The Studio chart gives its pod no preStop delay, so for a few seconds after a restart the ingress + * can still send a request to the pod that just stopped, which waits 30 seconds and ends in a 504. + */ +async function upgraded(sets: string[] = []): Promise { + const run = upgrade(sets); + expect(run.status, run.output).toBe(0); + for (const deploy of ["libredb-platform", "libredb-studio"]) kubectl(["rollout", "status", `deploy/${deploy}`, "--timeout=300s"]); + for (const selector of ["app.kubernetes.io/component=platform", "app.kubernetes.io/name=studio"]) { + await expect.poll(() => podCount(selector), { timeout: 180_000, intervals: [1_000] }).toBe(1); + } + await expect.poll(() => settled(`${STUDIO_URL}/login`, "307") && settled(`${PLATFORM_URL}/api/health/ready`, "200"), { timeout: 180_000, intervals: [1_000] }).toBe(true); +} + +interface Heartbeat { + kids: string[]; + canary: Record; +} + +/** The live heartbeats of bridge.studio_instance, as the runbook's query reads them. */ +function heartbeats(): Heartbeat[] { + return JSON.parse( + psqlAsSuperuser( + "libredb", + `SELECT coalesce(json_agg(json_build_object('kids', keyring_kids, 'canary', canary) ORDER BY instance_id), '[]') + FROM bridge.studio_instance WHERE last_seen_at > now() - interval '2 minutes'`, + ), + ) as Heartbeat[]; +} + +function envelopeKids(): { read: string; write: string } { + return JSON.parse( + psqlAsSuperuser( + "libredb", + `SELECT json_build_object('read', split_part(read_secret, '.', 2), 'write', split_part(write_secret, '.', 2)) + FROM bridge.connections_v1 WHERE connection_id = '${connection.id}'`, + ), + ) as { read: string; write: string }; +} + +function canaryKids(): string[] { + return JSON.parse(psqlAsSuperuser("libredb", "SELECT coalesce(json_agg(kid ORDER BY kid), '[]') FROM bridge.seal_canary_v1")) as string[]; +} + +/** The kid of BRIDGE_SEAL_PUBLIC_KEY in the running Platform pod. */ +function platformKid(): string { + return sealKeyId(kubectl(["exec", "deploy/libredb-platform", "--", "printenv", "BRIDGE_SEAL_PUBLIC_KEY"])); +} + +function query(): Promise { + return studioRequest(studio, "/api/db/query", { body: { connectionId: `seed:${connection.id}`, sql: "SELECT 1" } }); +} + +async function userStillQueries(): Promise { + await openStudioConsole(studio); + await expect.poll(async () => (await query()).status, { timeout: 90_000, intervals: [1_000] }).toBe(200); +} + +/** Removes one value from the secret store, as the guide's rotation does, so the next upgrade generates it again. */ +function deleteStored(name: string): string { + const value = secretValue("libredb-secrets", name); + expect(value, name).not.toBe(""); + kubectl(["patch", "secret", "libredb-secrets", "--type", "json", "-p", JSON.stringify([{ op: "remove", path: `/data/${name}` }])]); + return value; +} + +test.beforeAll(async ({ browser }) => { + const image = kubectl(["get", "statefulset", "libredb-postgres", "-o", "jsonpath={.spec.template.spec.containers[0].image}"]); + target = startTargetDatabase(image); + user = await createKeycloakUser(`rotation-${Date.now().toString(36)}`, "user"); + adminContext = await browser.newContext(); + userContext = await browser.newContext(); + platform = await adminContext.newPage(); + studio = await userContext.newPage(); + await signInFirstAdminToPlatform(platform, { + username: FIRST_ADMIN, + temporaryPassword: secretValue("libredb-keycloak-config", "first-admin-password"), + newPassword: `Rotation-${Date.now()}-Admin-Pass!`, + email: "admin@e2e.libredb.test", + firstName: "Rotation", + lastName: "Admin", + }); + const name = `rotation-${Date.now().toString(36)}`; + await createConnection(platform, { name, host: target.host, port: target.port, database: target.database, read: target.read, write: target.write }); + connection = { id: await openConnection(platform, name), name }; + await grantAccess(platform, user.username, "read"); + await signInToStudioAs(studio, user); + await userStillQueries(); +}); + +test.afterAll(async () => { + stopKeycloakForward(); + await adminContext?.close(); + await userContext?.close(); +}); + +test("seal key rotation: issue, switch, a refused early retirement, re-entry, retirement", async () => { + test.setTimeout(40 * 60_000); + const oldKid = envelopeKids().read; + expect(envelopeKids()).toEqual({ read: oldKid, write: oldKid }); + expect(platformKid()).toBe(oldKid); + + // 1. Add the new key to Studio. + sealSets = ["platform.sealKeys.issueNew=true"]; + await upgraded(); + let newKid = ""; + await expect + .poll( + () => { + const live = heartbeats(); + newKid = live[0]?.kids.find((kid) => kid !== oldKid) ?? ""; + return newKid !== "" && live.every((row) => row.kids.includes(oldKid) && row.kids.includes(newKid) && row.canary[oldKid] === "ok"); + }, + { timeout: HEARTBEAT_WAIT_MS, intervals: [2_000] }, + ) + .toBe(true); + expect(platformKid()).toBe(oldKid); + // The same values again issue no second key. + await upgraded(); + expect(heartbeats().every((row) => row.kids.length === 2)).toBe(true); + await userStillQueries(); + + // 2. Switch Platform to the new key; its canary for the new kid turns ok in every heartbeat. + sealSets = ["platform.sealKeys.issueNew=false", `platform.sealKeys.activeKid=${newKid}`]; + await upgraded(); + expect(platformKid()).toBe(newKid); + await expect + .poll(() => heartbeats().every((row) => row.canary[newKid] === "ok" && row.canary[oldKid] === "ok"), { + timeout: HEARTBEAT_WAIT_MS, + intervals: [2_000], + }) + .toBe(true); + await userStillQueries(); + + // Retiring before the re-entry: the bootstrap job refuses at once, naming the connection, and changes nothing. + const storeBefore = kubectl(["get", "secret", "libredb-secrets", "-o", "jsonpath={.data.seal-keys\\.json}"]); + const started = Date.now(); + const refused = upgrade([`platform.sealKeys.retire[0]=${oldKid}`]); + expect(refused.status, refused.output).not.toBe(0); + expect(Date.now() - started).toBeLessThan(5 * 60_000); + const log = kubectl(["logs", "job/libredb-bootstrap", "-c", "bootstrap"]); + expect(log).toContain(`${connection.name} (${connection.id})`); + expect(kubectl(["get", "secret", "libredb-secrets", "-o", "jsonpath={.data.seal-keys\\.json}"])).toBe(storeBefore); + expect(canaryKids()).toContain(oldKid); + await userStillQueries(); + + // 3. Platform warns about the credentials sealed to the old key; the admin enters them again. + await platform.goto(`/connections/${connection.id}`); + await expect(platform.getByRole("alert").filter({ hasText: "sealed to an earlier Studio key" })).toContainText(oldKid); + const readGroup = platform.getByRole("group", { name: "Read credential" }); + await readGroup.getByLabel("User", { exact: true }).fill(target.read.user); + await readGroup.getByLabel("Password", { exact: true }).fill(target.read.password); + await platform.getByLabel(/I confirm that the read credential can only read/).check(); + await platform.getByLabel("Write credential", { exact: true }).selectOption("set"); + const writeGroup = platform.getByRole("group", { name: "New write credential" }); + await writeGroup.getByLabel("User", { exact: true }).fill((target.write as Credential).user); + await writeGroup.getByLabel("Password", { exact: true }).fill((target.write as Credential).password); + await platform.getByRole("button", { name: "Save changes" }).click(); + await expect.poll(() => envelopeKids(), { timeout: 30_000, intervals: [500] }).toEqual({ read: newKid, write: newKid }); + await userStillQueries(); + + // 4. Retire the old key: out of every keyring, its canary row gone, and the user still queries. + sealSets = ["platform.sealKeys.issueNew=false", `platform.sealKeys.activeKid=${newKid}`, `platform.sealKeys.retire[0]=${oldKid}`]; + await upgraded(); + expect(canaryKids()).not.toContain(oldKid); + expect(canaryKids()).toContain(newKid); + await expect + .poll( + () => heartbeats().map((row) => ({ old: row.kids.includes(oldKid) || oldKid in row.canary, current: row.canary[newKid] })), + { timeout: HEARTBEAT_WAIT_MS, intervals: [2_000] }, + ) + .toEqual([{ old: false, current: "ok" }]); + const stored = JSON.parse(secretValue("libredb-secrets", "seal-keys.json")) as { kid: string; state: string }[]; + expect(stored.map((key) => [key.kid, key.state])).toEqual([ + [oldKid, "retired"], + [newKid, "live"], + ]); + expect(platformKid()).toBe(newKid); + await userStillQueries(); +}); + +test("Studio's JWT_SECRET: a new value ends every Studio session, and Keycloak's session signs the user in again", async () => { + test.setTimeout(15 * 60_000); + const old = deleteStored("studio-jwt-secret"); + await upgraded(); + expect(secretValue("libredb-studio-env", "jwt-secret")).not.toBe(old); + await openStudioConsole(studio); + await expect.poll(async () => (await query()).status, { timeout: 60_000 }).toBe(401); + // The Keycloak session is still alive, so the sign-in completes without the password form, as the runbook says. + await studio.goto(`${STUDIO_URL}/login`); + await expect(studio).toHaveURL((url) => url.origin === STUDIO_URL && url.pathname !== "/login", { timeout: 30_000 }); + await expect(studio.locator("#password")).toHaveCount(0); + await userStillQueries(); +}); + +test("Platform's AUTH_SECRET with its previous value: the admin's session survives the rotation", async () => { + test.setTimeout(15 * 60_000); + const old = deleteStored("platform-auth-secret"); + kubectl(["create", "secret", "generic", "libredb-auth-previous", `--from-literal=auth-secret-previous=${old}`]); + await upgraded(["platform.authSecretPrevious.existingSecret=libredb-auth-previous"]); + const current = kubectl(["exec", "deploy/libredb-platform", "--", "printenv", "AUTH_SECRET"]); + expect(current).not.toBe(old); + expect(kubectl(["exec", "deploy/libredb-platform", "--", "printenv", "AUTH_SECRET_PREVIOUS"])).toBe(old); + await platform.goto(`/connections/${connection.id}`); + await expect(platform).toHaveURL((url) => url.origin === PLATFORM_URL && url.pathname === `/connections/${connection.id}`); + // After the session lifetime, the previous value goes. + await upgraded(); + expect(spawnSync("kubectl", ["-n", NAMESPACE, "exec", "deploy/libredb-platform", "--", "printenv", "AUTH_SECRET_PREVIOUS"]).status).not.toBe(0); + kubectl(["delete", "secret", "libredb-auth-previous"]); +}); + +test("Studio's Keycloak client secret and libredb_platform's password: new values reach Keycloak, PostgreSQL and the apps", async () => { + test.setTimeout(15 * 60_000); + const client = deleteStored("keycloak-client-libredb-studio"); + const role = deleteStored("postgres-platform-password"); + await upgraded(); + expect(secretValue("libredb-studio-env", "oidc-client-secret")).not.toBe(client); + expect(kubectl(["exec", "deploy/libredb-platform", "--", "printenv", "DATABASE_URL"])).not.toContain(role); + // A new Studio sign-in goes through Keycloak with the new client secret. + await studio.context().clearCookies(); + await signInToStudioAs(studio, user); + await userStillQueries(); + // Platform reads its database with the new password. + await platform.goto("/audit"); + await expect(platform).toHaveURL((url) => url.pathname === "/audit"); + expect(execFileSync("curl", ["-sk", "--resolve", `${new URL(PLATFORM_URL).host}:443:127.0.0.1`, "-o", "/dev/null", "-w", "%{http_code}", `${PLATFORM_URL}/api/health/ready`], { encoding: "utf8" })).toBe("200"); +}); diff --git a/installer/package.json b/installer/package.json index a40f8fa..4a7a8e6 100644 --- a/installer/package.json +++ b/installer/package.json @@ -15,7 +15,8 @@ "embed": "bun scripts/embed-release.ts", "release:check": "bun scripts/check-release-compat.ts", "release:record": "bun scripts/record-release.ts", - "build": "bun scripts/build.ts" + "build": "bun scripts/build.ts", + "build:helm-jobs": "bun scripts/build-helm-jobs.ts" }, "devDependencies": { "@playwright/test": "1.58.2", diff --git a/installer/scripts/build-helm-jobs.ts b/installer/scripts/build-helm-jobs.ts new file mode 100644 index 0000000..c50ffae --- /dev/null +++ b/installer/scripts/build-helm-jobs.ts @@ -0,0 +1,48 @@ +// Bundles the Helm chart's jobs (src/cli/kubernetes-job.ts) into one CommonJS file for the Platform image's node, +// gzipped as charts/libredb-platform/files/libredb-jobs.cjs.gz, which the chart mounts from a ConfigMap: compressed, so +// the Helm release record stays far below the 1 MiB of a Secret. The bundle is generated: +// test/unit/kubernetes/bundle.test.ts rebuilds it and fails when the committed file differs, so the chart always runs +// the installer's current code. Run `bun run build:helm-jobs` after changing anything the jobs import. +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { gunzipSync, gzipSync } from "node:zlib"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = join(import.meta.dir, ".."); +export const ENTRYPOINT = join(root, "src/cli/kubernetes-job.ts"); +export const CHART_BUNDLE = join(root, "..", "charts", "libredb-platform", "files", "libredb-jobs.cjs.gz"); + +const BANNER = + "// Generated by installer/scripts/build-helm-jobs.ts from installer/src/cli/kubernetes-job.ts; do not edit.\n"; + +/** Builds the bundle and returns its content. */ +export function buildHelmJobs(): string { + const dir = mkdtempSync(join(tmpdir(), "libredb-helm-jobs-")); + try { + const outfile = join(dir, "libredb-jobs.cjs"); + const result = Bun.spawnSync( + ["bun", "build", "--target=node", "--format=cjs", "--minify", ENTRYPOINT, "--outfile", outfile], + { cwd: root, stdout: "pipe", stderr: "pipe" }, + ); + if (result.exitCode !== 0) throw new Error(`bun build failed:\n${result.stderr.toString()}`); + return BANNER + readFileSync(outfile, "utf8"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +/** The bundle as the chart ships it: gzip at level 9, whose header carries no file name and no time. */ +export function compressBundle(content: string): Buffer { + return gzipSync(Buffer.from(content, "utf8"), { level: 9 }); +} + +/** The committed bundle, decompressed. */ +export function committedBundle(): string { + return gunzipSync(readFileSync(CHART_BUNDLE)).toString("utf8"); +} + +if (import.meta.main) { + const compressed = compressBundle(buildHelmJobs()); + writeFileSync(CHART_BUNDLE, compressed); + console.log(`wrote ${CHART_BUNDLE} (${compressed.length} bytes)`); +} diff --git a/installer/src/adapters/kubernetes-api.ts b/installer/src/adapters/kubernetes-api.ts new file mode 100644 index 0000000..d0707dc --- /dev/null +++ b/installer/src/adapters/kubernetes-api.ts @@ -0,0 +1,222 @@ +// The Kubernetes API of the chart's jobs: the namespace's Secrets and ConfigMaps through the in-cluster service +// account. The job's container trusts the cluster CA through NODE_EXTRA_CA_CERTS, so plain fetch reaches the API +// server. A replace carries the resourceVersion the job read, so two jobs never overwrite each other's write, and +// keeps the labels and annotations others gave the object. Every object the jobs write is marked so that neither +// Helm nor Argo CD prunes it: it holds the installation's secrets, which must outlive the release. +import type { KubernetesObjects, ObjectData } from "../kubernetes/bootstrap"; +import type { Workload } from "../kubernetes/names"; + +/** Argo CD keeps an object with these annotations through a prune and leaves it out of the app's sync status. */ +export const KEEP_ANNOTATIONS = { + "argocd.argoproj.io/sync-options": "Prune=false,Delete=false", + "argocd.argoproj.io/compare-options": "IgnoreExtraneous", +} as const; + +/** The label by which Helm-aware tools track an object as part of a release; the jobs' objects never carry it. */ +export const TRACKING_LABEL = "app.kubernetes.io/instance"; + +/** The pod template annotation a restart sets, as `kubectl rollout restart` does with its own. */ +export const RESTARTED_AT = "install.libredb.org/restarted-at"; + +const TIMEOUT_MS = 30_000; + +/** The files every pod with a service account token reads. */ +export const SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount"; + +export class KubernetesApiError extends Error { + constructor(method: string, path: string, status: number, reason: string) { + super(`Kubernetes API ${method} ${path} answered ${status}${reason === "" ? "" : `: ${reason}`}`); + this.name = "KubernetesApiError"; + } +} + +export interface KubernetesApiOptions { + /** https://: of the API server. */ + server: string; + token: string; + namespace: string; + /** The labels every object the job writes carries. */ + labels: Readonly>; + fetch?: typeof fetch; + log?: (line: string) => void; +} + +interface ObjectMeta { + name: string; + resourceVersion?: string; + labels?: Record; + annotations?: Record; +} + +function encode(data: ObjectData): Record { + return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, Buffer.from(value, "utf8").toString("base64")])); +} + +function decode(data: Record | undefined): ObjectData { + return Object.fromEntries(Object.entries(data ?? {}).map(([key, value]) => [key, Buffer.from(value, "base64").toString("utf8")])); +} + +async function reasonOf(response: Response): Promise { + try { + const body = (await response.json()) as { message?: unknown }; + return typeof body.message === "string" ? body.message : ""; + } catch { + return ""; + } +} + +export class KubernetesApi implements KubernetesObjects { + readonly #options: KubernetesApiOptions; + readonly #versions = new Map(); + readonly #metadata = new Map(); + readonly #types = new Map(); + + constructor(options: KubernetesApiOptions) { + this.#options = options; + } + + /** The API of the pod's own service account, from the standard environment and files. */ + static inCluster( + env: Readonly>, + readFile: (path: string) => string | undefined, + labels: Readonly>, + log?: (line: string) => void, + ): KubernetesApi { + const host = env.KUBERNETES_SERVICE_HOST; + const port = env.KUBERNETES_SERVICE_PORT ?? "443"; + const token = readFile(`${SERVICE_ACCOUNT_DIR}/token`)?.trim(); + const namespace = readFile(`${SERVICE_ACCOUNT_DIR}/namespace`)?.trim(); + if (host === undefined || token === undefined || namespace === undefined) { + throw new Error("not running in a Kubernetes pod with a service account token: KUBERNETES_SERVICE_HOST or the token is missing"); + } + const server = host.includes(":") ? `https://[${host}]:${port}` : `https://${host}:${port}`; + return new KubernetesApi({ server, token, namespace, labels, ...(log === undefined ? {} : { log }) }); + } + + get namespace(): string { + return this.#options.namespace; + } + + async #request(method: string, path: string, body?: unknown, contentType = "application/json"): Promise { + const response = await (this.#options.fetch ?? fetch)(`${this.#options.server}${path}`, { + method, + headers: { + authorization: `Bearer ${this.#options.token}`, + accept: "application/json", + ...(body === undefined ? {} : { "content-type": contentType }), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + this.#options.log?.(`kubernetes ${method} ${path} ${response.status}`); + return response; + } + + #path(kind: "secrets" | "configmaps", name?: string): string { + const base = `/api/v1/namespaces/${encodeURIComponent(this.#options.namespace)}/${kind}`; + return name === undefined ? base : `${base}/${encodeURIComponent(name)}`; + } + + async #get( + kind: "secrets" | "configmaps", + name: string, + ): Promise<{ metadata: ObjectMeta; data?: Record; type?: string } | undefined> { + const path = this.#path(kind, name); + const response = await this.#request("GET", path); + if (response.status === 404) { + await response.body?.cancel(); + this.#versions.delete(`${kind}/${name}`); + return undefined; + } + if (!response.ok) throw new KubernetesApiError("GET", path, response.status, await reasonOf(response)); + const object = (await response.json()) as { metadata: ObjectMeta; data?: Record; type?: string }; + if (object.metadata.resourceVersion !== undefined) this.#versions.set(`${kind}/${name}`, object.metadata.resourceVersion); + this.#metadata.set(`${kind}/${name}`, object.metadata); + if (object.type !== undefined) this.#types.set(`${kind}/${name}`, object.type); + return object; + } + + async #put(kind: "secrets" | "configmaps", name: string, body: Record): Promise { + const key = `${kind}/${name}`; + if (!this.#versions.has(key)) await this.#get(kind, name); + const version = this.#versions.get(key); + const existingType = this.#types.get(key); + if (version !== undefined && body.type !== undefined && existingType !== undefined && existingType !== body.type) { + throw new KubernetesApiError( + "PUT", + this.#path(kind, name), + 422, + `Secret ${name} exists with type ${existingType}, not ${String(body.type)}; it is in the way of the installation, so delete or rename it`, + ); + } + const previous = this.#metadata.get(key); + // An object an earlier build wrote may carry Helm's instance label, which a tracking tool would prune by. + const { [TRACKING_LABEL]: _tracking, ...kept } = previous?.labels ?? {}; + const metadata: ObjectMeta = { + name, + labels: { ...kept, ...this.#options.labels }, + annotations: { ...previous?.annotations, ...KEEP_ANNOTATIONS }, + }; + const object = { ...body, metadata: version === undefined ? metadata : { ...metadata, resourceVersion: version } }; + const path = version === undefined ? this.#path(kind) : this.#path(kind, name); + const method = version === undefined ? "POST" : "PUT"; + const response = await this.#request(method, path, object); + if (!response.ok) throw new KubernetesApiError(method, path, response.status, await reasonOf(response)); + const written = (await response.json()) as { metadata: ObjectMeta; type?: string }; + if (written.metadata.resourceVersion !== undefined) this.#versions.set(key, written.metadata.resourceVersion); + this.#metadata.set(key, written.metadata); + if (written.type !== undefined) this.#types.set(key, written.type); + } + + async getSecret(name: string): Promise { + const object = await this.#get("secrets", name); + return object === undefined ? undefined : decode(object.data); + } + + async getConfigMap(name: string): Promise { + return (await this.#get("configmaps", name))?.data; + } + + async restart(workload: Workload, at: Date): Promise<"restarted" | "absent"> { + const path = `/apis/apps/v1/namespaces/${encodeURIComponent(this.#options.namespace)}/${workload.kind}/${encodeURIComponent(workload.name)}`; + const patch = { spec: { template: { metadata: { annotations: { [RESTARTED_AT]: at.toISOString() } } } } }; + const response = await this.#request("PATCH", path, patch, "application/merge-patch+json"); + if (response.status === 404) { + await response.body?.cancel(); + return "absent"; + } + if (!response.ok) throw new KubernetesApiError("PATCH", path, response.status, await reasonOf(response)); + await response.body?.cancel(); + return "restarted"; + } + + async podsCreated(workload: Workload): Promise { + const namespace = encodeURIComponent(this.#options.namespace); + const path = `/apis/apps/v1/namespaces/${namespace}/${workload.kind}/${encodeURIComponent(workload.name)}`; + const response = await this.#request("GET", path); + if (response.status === 404) { + await response.body?.cancel(); + return undefined; + } + if (!response.ok) throw new KubernetesApiError("GET", path, response.status, await reasonOf(response)); + const object = (await response.json()) as { spec: { selector: { matchLabels: Record } } }; + const selector = Object.entries(object.spec.selector.matchLabels).map(([key, value]) => `${key}=${value}`).join(","); + const podsPath = `/api/v1/namespaces/${namespace}/pods?labelSelector=${encodeURIComponent(selector)}`; + const pods = await this.#request("GET", podsPath); + if (!pods.ok) throw new KubernetesApiError("GET", podsPath, pods.status, await reasonOf(pods)); + const list = (await pods.json()) as { items: { metadata: { creationTimestamp: string } }[] }; + return list.items.map((pod) => new Date(pod.metadata.creationTimestamp)); + } + + async putSecret(name: string, data: ObjectData, type: "Opaque" | "kubernetes.io/tls" = "Opaque"): Promise { + await this.#put("secrets", name, { apiVersion: "v1", kind: "Secret", type, data: encode(data) }); + } + + async putConfigMap(name: string, data: ObjectData, binaryData?: Readonly>): Promise { + const binary = + binaryData === undefined + ? {} + : { binaryData: Object.fromEntries(Object.entries(binaryData).map(([key, value]) => [key, Buffer.from(value).toString("base64")])) }; + await this.#put("configmaps", name, { apiVersion: "v1", kind: "ConfigMap", data, ...binary }); + } +} diff --git a/installer/src/cli/kubernetes-job.ts b/installer/src/cli/kubernetes-job.ts new file mode 100644 index 0000000..6596ebd --- /dev/null +++ b/installer/src/cli/kubernetes-job.ts @@ -0,0 +1,114 @@ +// The entry point of the Helm chart's jobs, bundled by scripts/build-helm-jobs.ts into +// charts/libredb-platform/files/libredb-jobs.cjs and run by the Platform image's node: +// +// node libredb-jobs.cjs bootstrap the pre-install and pre-upgrade hook: secrets, seal keys, TLS, envs +// node libredb-jobs.cjs keycloak the keycloak-configure job +// node libredb-jobs.cjs restart the post-install and post-upgrade hook: the restarts the bootstrap asked for +// +// The installation document is the chart's rendering of the Helm values as a LibreDBInstallation of mode kubernetes, +// validated by the installer's own schema. Every line the job prints passes through the redactor. +import { readFileSync, writeFileSync } from "node:fs"; +import { KubernetesApi } from "../adapters/kubernetes-api"; +import { ConfigError, parseInstallation } from "../config/schema"; +import { ExitCode } from "./exit-codes"; +import { runBootstrap, runRestarts } from "../kubernetes/bootstrap"; +import { JOBS_BUNDLE_KEY } from "../kubernetes/names"; +import { runKeycloakJob } from "../kubernetes/keycloak"; +import { Redactor } from "../log/redact"; +import { parseReleaseManifest } from "../release/manifest"; +import { applyKeycloak, observeKeycloak } from "../steps/keycloak-configure"; +import manifestDocument from "../../release/release.config.json" with { type: "json" }; + +/** Where the chart mounts its ConfigMap with installation.json and chart.json. */ +export const CHART_CONFIG_DIR = "/etc/libredb/chart"; + +/** Where the chart mounts the gzipped bundle this file was decompressed from. */ +export const JOBS_DIR = "/etc/libredb/jobs"; + +function readOptional(path: string): string | undefined { + try { + return readFileSync(path, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } +} + +function readJson(path: string): unknown { + const text = readOptional(path); + if (text === undefined) throw new Error(`${path} is missing: the chart mounts it from its ConfigMap`); + return JSON.parse(text); +} + +/** The error's message with its causes, such as the connection error behind `fetch failed`. */ +export function describeError(error: unknown): string { + const parts: string[] = []; + for (let current: unknown = error; current !== undefined && parts.length < 4; current = (current as { cause?: unknown }).cause) { + parts.push(current instanceof Error ? current.message : String(current)); + if (!(current instanceof Error)) break; + } + return parts.join(": "); +} + +/** + * The job's exit code: the installer's code of a refusal that a retry cannot change (an invalid installation, a failed + * precondition, an unsafe change), on which the chart's podFailurePolicy fails the Job at once, and 1 otherwise. + */ +export function jobExitCode(error: unknown): number { + if (error instanceof ConfigError) return ExitCode.InvalidUsage; + const code = (error as { exitCode?: unknown }).exitCode; + const final: readonly number[] = [ExitCode.InvalidUsage, ExitCode.PreflightFailed, ExitCode.UnsafeChangeRefused]; + return typeof code === "number" && final.includes(code) ? code : ExitCode.RuntimeFailure; +} + +async function main(command: string | undefined): Promise { + const redactor = new Redactor(); + const log = (line: string) => console.log(redactor.redact(line)); + try { + const chart = readJson(`${CHART_CONFIG_DIR}/chart.json`) as { studioSecretName?: unknown; labels?: unknown }; + const labels = (chart.labels ?? {}) as Record; + const manifest = parseReleaseManifest(manifestDocument); + if (command === "restart") { + const objects = KubernetesApi.inCluster(process.env, readOptional, labels); + await runRestarts(objects, manifest.version, () => new Date(), log); + return; + } + const config = parseInstallation(readJson(`${CHART_CONFIG_DIR}/installation.json`)); + if (typeof chart.studioSecretName !== "string" || chart.studioSecretName === "") { + throw new Error(`${CHART_CONFIG_DIR}/chart.json names no studioSecretName`); + } + const objects = KubernetesApi.inCluster(process.env, readOptional, labels); + if (command === "bootstrap") { + await runBootstrap( + { config, manifest, studioSecretName: chart.studioSecretName, jobsBundle: readFileSync(`${JOBS_DIR}/${JOBS_BUNDLE_KEY}`) }, + { objects, readFile: readOptional, env: process.env, now: () => new Date(), log, redactor }, + ); + } else if (command === "keycloak") { + await runKeycloakJob( + config, + manifest, + { + objects, + readFile: readOptional, + writeFile: (path, content) => writeFileSync(path, content, { mode: 0o600 }), + env: process.env, + tmpDir: "/tmp", + owner: { uid: process.getuid?.() ?? 0, gid: process.getgid?.() ?? 0 }, + log, + redactor, + }, + { observe: observeKeycloak, apply: applyKeycloak }, + ); + } else { + console.error("usage: libredb-jobs.cjs bootstrap|keycloak|restart"); + process.exitCode = 64; + return; + } + } catch (error) { + const message = error instanceof ConfigError ? `invalid installation:\n${error.message}` : describeError(error); + console.error(redactor.redact(`error: ${message}`)); + process.exitCode = jobExitCode(error); + } +} + +void main(process.argv[2]); diff --git a/installer/src/kubernetes/bootstrap.ts b/installer/src/kubernetes/bootstrap.ts new file mode 100644 index 0000000..36bce4f --- /dev/null +++ b/installer/src/kubernetes/bootstrap.ts @@ -0,0 +1,612 @@ +// The bootstrap job of the Helm chart charts/libredb-platform: the Kubernetes counterpart of the installer's secrets, +// tls and env steps. It runs as a pre-install and pre-upgrade hook before any app starts, with the installation's +// configuration as the chart rendered it, and writes every Secret and ConfigMap the chart's workloads mount. Every +// value comes from the installer's own modules, so a Helm install gets the same URLs, environments, certificates, +// scripts and keyring as a Docker install of the same configuration. +// A generated secret, a seal key or the internal CA is created once and never replaced; the derived objects are +// written again on every run. +import type { LibreDBInstallation } from "../config/schema"; +import { readSecretReference } from "../config/secret-ref"; +import { APP_SECRET_NAMES, dockerSecretNames, POSTGRES_SUPERUSER_SECRET, siteSecretReferences } from "../docker/secrets"; +import { CLIENT_SECRET_NAMES } from "../keycloak/desired"; +import type { Redactor } from "../log/redact"; +import { computeDesired, type DesiredInstallation } from "../model/desired"; +import { readAuthSecretPrevious, readPlatformLicense, renderPlatformEnv, renderPlatformMigrateEnv } from "../model/platform-env"; +import { renderStudioEnv } from "../model/studio-env"; +import type { ReleaseManifest } from "../release/manifest"; +import { renderCaddyfile } from "../render/caddyfile"; +import { renderPgHba } from "../render/compose"; +import { generateSecret } from "../secrets/generate"; +import { renderKeyring } from "../secrets/keyring"; +import { activeSealKey, parseEnvelopeUses, planSealRotation, sealKidsToRetire, type EnvelopeUse } from "../secrets/seal-rotation"; +import { generateSealKeyPair } from "../secrets/seal-keys"; +import type { SealKeyRecord } from "../secrets/store"; +import { renderDbaScript, renderProvisionScript, ROLE_SPECS, roleVerifiers, expectedRoles } from "../sql/provision"; +import { checkImmutable, immutableFieldsOf } from "../state/immutable"; +import { emptyState, parseState, type InstallerState } from "../state/store"; +import { buildCaBundle, caBundleFiles, postgresCaBundleFile, type CaBundleInput } from "../tls/bundle"; +import { createCa, type KeyedCertificate } from "../tls/ca"; +import { issueLeaf } from "../tls/leaf"; +import { desiredTls, planTls, type ObservedTls } from "../tls/plan"; +import { ExitCode } from "../cli/exit-codes"; +import { compareReleaseVersions } from "../release/compat"; +import { + BOOTSTRAP_CLIENT, + JOBS_BUNDLE_KEY, + K8S_NAMES, + KEYCLOAK_EDGE_PORT, + STORE_KEYS, + STUDIO_SECRET_KEYS, + WORKLOADS, + type Workload, +} from "./names"; + +/** + * A run the configuration or the installation's history refuses, which a retry cannot fix. It carries the installer's + * exit code, on which the chart's jobs fail at once instead of retrying (podFailurePolicy). + */ +export class RefusedError extends Error { + readonly exitCode: ExitCode; + + constructor(message: string, exitCode: ExitCode) { + super(message); + this.name = "RefusedError"; + this.exitCode = exitCode; + } +} + +/** Secret data by key, as plain text; the adapter encodes and decodes base64. */ +export type ObjectData = Record; + +/** The namespace's Secrets and ConfigMaps, which the adapter reaches through the Kubernetes API. */ +export interface KubernetesObjects { + getSecret(name: string): Promise; + getConfigMap(name: string): Promise; + /** Restarts a Deployment or StatefulSet by a pod template annotation, or reports it absent. */ + restart(workload: Workload, at: Date): Promise<"restarted" | "absent">; + /** When each pod of a workload was created, or undefined when the workload does not exist. */ + podsCreated(workload: Workload): Promise; + /** Creates the Secret or replaces its data. */ + putSecret(name: string, data: ObjectData, type?: "Opaque" | "kubernetes.io/tls"): Promise; + /** Creates the ConfigMap or replaces its data and its binary data. */ + putConfigMap(name: string, data: ObjectData, binaryData?: Readonly>): Promise; +} + +export interface BootstrapInput { + config: LibreDBInstallation; + manifest: ReleaseManifest; + /** The Secret the Studio chart reads as secrets.existingSecret. */ + studioSecretName: string; + /** The jobs' gzipped bundle as the hook mounts it, which the keycloak-configure job runs from ConfigMap libredb-jobs. */ + jobsBundle?: Uint8Array; +} + +export interface BootstrapIO { + objects: KubernetesObjects; + /** The content of a mounted input file, or undefined when it does not exist. */ + readFile(path: string): string | undefined; + env: Readonly>; + now(): Date; + log(line: string): void; + redactor: Redactor; +} + +/** The state Secret's key. */ +export const STATE_KEY = "state.json"; + +/** + * The state Secret's key of the Platform release the installation last ran, which a downgrade may not go below. The + * restart job records it once an install or upgrade succeeded, so the rollback that `helm upgrade --atomic` runs after + * a failed upgrade goes back to a release the installation still counts as its own. + */ +export const RELEASE_KEY = "release"; + +/** + * The state Secret's key of the restarts the bootstrap job asks for: the workloads whose inputs it changed and when. + * The restart job runs after Helm applied the release, so a workload restarts once, with the release's pod template, + * and a restart a failed run did not finish stays asked for. + */ +export const PENDING_RESTARTS_KEY = "pending-restarts"; + +export interface PendingRestarts { + /** When the inputs last changed: a pod created after it already read them. */ + since: string; + workloads: Workload[]; +} + +/** The pending restarts in the state Secret, or none. */ +export function pendingRestartsOf(data: ObjectData | undefined): PendingRestarts | undefined { + const text = data?.[PENDING_RESTARTS_KEY]; + return text === undefined || text === "" ? undefined : (JSON.parse(text) as PendingRestarts); +} + +/** Adds workloads to the pending restarts, with the time of this change. */ +export function addPendingRestarts(previous: PendingRestarts | undefined, workloads: readonly Workload[], at: Date): PendingRestarts { + const all = new Map(); + for (const workload of [...(previous?.workloads ?? []), ...workloads]) all.set(`${workload.kind}/${workload.name}`, workload); + return { since: at.toISOString(), workloads: [...all.values()] }; +} + +/** Loads the installer state from its Secret, or an empty state for a new installation. */ +export async function loadState(objects: KubernetesObjects): Promise { + const data = await objects.getSecret(K8S_NAMES.state); + const text = data?.[STATE_KEY]; + return text === undefined ? emptyState() : parseState(text, `Secret ${K8S_NAMES.state}`); +} + +/** Saves the state, keeping the other keys of its Secret, such as the release. */ +export async function saveState(objects: KubernetesObjects, state: InstallerState, extra: ObjectData = {}): Promise { + const current = (await objects.getSecret(K8S_NAMES.state)) ?? {}; + await objects.putSecret(K8S_NAMES.state, { ...current, ...extra, [STATE_KEY]: `${JSON.stringify(state, null, 2)}\n` }); +} + +/** + * Refuses to run an older release than the installation last ran: its migrations and its seal and realm logic would + * meet the newer schema and objects. A downgrade, a `helm rollback` included, restores from a backup instead. + */ +export async function checkRelease(objects: KubernetesObjects, version: string): Promise { + const recorded = (await objects.getSecret(K8S_NAMES.state))?.[RELEASE_KEY]; + if (recorded !== undefined && compareReleaseVersions(version, recorded) < 0) { + throw new RefusedError( + `this chart runs LibreDB ${version}, and the installation already ran ${recorded}: a downgrade is not supported, ` + + "restore the installation from a backup instead", + ExitCode.UnsafeChangeRefused, + ); + } +} + +/** + * Records the immutable fields on the first run and refuses a later run that changes one, such as postgres.mode or + * keycloak.realm, with the installer's UnsafeChangeError. + */ +export function checkInstallation(state: InstallerState, config: LibreDBInstallation): InstallerState { + const desired = immutableFieldsOf(config); + if (state.installation !== undefined) checkImmutable(state.installation, desired); + return { ...state, installation: desired }; +} + +/** The names of the generated secrets: the installer's set of the configuration plus the chart's own. */ +export function generatedNames(config: LibreDBInstallation): string[] { + const names = dockerSecretNames(config); + if (config.keycloak.mode === "managed") names.push(STORE_KEYS.keycloakBootstrapSecret); + if (config.keycloak.firstAdmin.enabled) names.push(STORE_KEYS.firstAdminPassword); + if (config.keycloak.userManager.enabled) names.push(STORE_KEYS.userManagerPassword); + return names; +} + +/** + * Where the chart's seal-envelopes init container writes, for a run whose bridge.sealKeys.retire names a kid, psql's + * output of envelopeUsesScript: the connections in bridge.connections_v1 whose envelopes those kids sealed. + */ +export const SEAL_ENVELOPES_FILE = "/tmp/seal-envelopes.out"; + +/** + * The seal keys after bridge.sealKeys, as the installer's secrets step rotates them: each live kid of retire becomes + * retired, which keeps it in the store and out of Studio's keyring, and issueNew adds one live key while the active key + * is the newest live one, so the same values issue no second key. A retirement is refused with exit 6 while an + * envelope still uses the kid; the chart's restart job deletes a retired kid's canary row once the release applied. + */ +export function rotateSealKeys( + keys: readonly SealKeyRecord[], + config: LibreDBInstallation, + readFile: (path: string) => string | undefined, +): { keys: SealKeyRecord[]; done: string[] } { + const retiring = sealKidsToRetire(keys, config.bridge.sealKeys); + let envelopes: EnvelopeUse[] = []; + if (retiring.length > 0) { + const uses = readFile(SEAL_ENVELOPES_FILE); + if (uses === undefined) { + throw new Error( + `retiring seal key ${retiring.join(", ")} needs the connections each still seals, which the chart's ` + + `seal-envelopes init container writes to ${SEAL_ENVELOPES_FILE}, and it wrote none`, + ); + } + envelopes = parseEnvelopeUses(uses); + } + let rotated = [...keys]; + const done: string[] = []; + for (const action of planSealRotation(keys, config.bridge.sealKeys, envelopes)) { + if (action.kind === "retire-seal-key") { + rotated = rotated.map((key) => (key.kid === action.kid ? { ...key, state: "retired" } : key)); + done.push(`retired seal key ${action.kid}: out of the Studio keyring, kept in the secret store`); + } else if (action.kind === "issue-seal-key") { + const issued: SealKeyRecord = { ...generateSealKeyPair(), state: "live" }; + rotated.push(issued); + done.push(`issued seal key ${issued.kid} into the Studio keyring`); + } + } + return { keys: rotated, done }; +} + +/** Parses the stored seal keys; a new store gets its first live key. */ +export function sealKeysOf(stored: string | undefined): { keys: SealKeyRecord[]; issued: boolean } { + if (stored === undefined) return { keys: [{ ...generateSealKeyPair(), state: "live" }], issued: true }; + const parsed: unknown = JSON.parse(stored); + if (!Array.isArray(parsed) || parsed.length === 0) { + throw new Error(`${STORE_KEYS.sealKeys} of Secret ${K8S_NAMES.secretStore} holds no seal key; restore it from a backup`); + } + return { keys: parsed as SealKeyRecord[], issued: false }; +} + +/** + * The secret store after this run: every missing generated secret drawn once, the site-supplied client secrets of + * keycloak.external.provisioning manual taken from their references, and the seal keys. Never replaces a stored value + * that the run generated before. + */ +export function convergeStore( + config: LibreDBInstallation, + stored: ObjectData, + io: Pick, +): { store: ObjectData; changed: string[]; sealKeys: SealKeyRecord[]; rotated: string[] } { + const store: ObjectData = { ...stored }; + const changed: string[] = []; + for (const name of generatedNames(config)) { + if (store[name] !== undefined && store[name] !== "") continue; + store[name] = generateSecret(); + changed.push(name); + } + for (const site of siteSecretReferences(config)) { + const lookup = readSecretReference(site.reference, { env: io.env, readFile: io.readFile }); + if ("missing" in lookup) throw new Error(`${site.reference.path}: ${lookup.missing}`); + if (store[site.name] === lookup.value) continue; + store[site.name] = lookup.value; + changed.push(site.name); + } + const seal = sealKeysOf(store[STORE_KEYS.sealKeys]); + const rotation = rotateSealKeys(seal.keys, config, io.readFile); + if (seal.issued || rotation.done.length > 0) { + store[STORE_KEYS.sealKeys] = JSON.stringify(rotation.keys); + changed.push(STORE_KEYS.sealKeys); + } + for (const [key, value] of Object.entries(store)) { + if (key !== STORE_KEYS.sealKeys && value !== "") io.redactor.register(value); + } + for (const key of rotation.keys) io.redactor.register(key.privateKey); + return { store, changed, sealKeys: rotation.keys, rotated: rotation.done }; +} + +const CA_CERT = "ca.crt"; +const CA_KEY = "ca.key"; +const leafCert = (name: string) => `${name}.crt`; +const leafKey = (name: string) => `${name}.key`; + +/** The TLS material in Secret libredb-tls as the installer's tls plan observes it. */ +export function observedTls(data: ObjectData, leafNames: readonly string[]): ObservedTls { + const pair = (cert: string, key: string): Partial => ({ + ...(data[cert] === undefined ? {} : { certPem: data[cert] }), + ...(data[key] === undefined ? {} : { keyPem: data[key] }), + }); + return { + ca: pair(CA_CERT, CA_KEY), + leaves: Object.fromEntries(leafNames.map((name) => [name, pair(leafCert(name), leafKey(name))])), + }; +} + +/** + * Creates the internal CA when it is missing and issues every leaf the installer's tls plan issues: missing, + * unreadable, not covering its hosts, not chaining to the CA, or with fewer than 30 days left. A present CA is never + * replaced. + */ +export async function convergeTls( + config: LibreDBInstallation, + stored: ObjectData, + now: Date, + log: (line: string) => void, +): Promise<{ data: ObjectData; changed: boolean }> { + const desired = desiredTls(config); + const plan = planTls(observedTls(stored, desired.leaves.map((leaf) => leaf.name)), desired, now); + const data: ObjectData = { ...stored }; + let changed = false; + if (plan.ca === "create") { + const ca = await createCa({ commonName: desired.caCommonName, now }); + data[CA_CERT] = ca.certPem; + data[CA_KEY] = ca.keyPem; + changed = true; + log(`tls: created the internal CA "${desired.caCommonName}"`); + } + const ca = { certPem: data[CA_CERT] as string, keyPem: data[CA_KEY] as string }; + for (const leaf of plan.leaves) { + if (leaf.action === "keep") continue; + const issued = await issueLeaf(ca, leaf.hosts, now); + data[leafCert(leaf.name)] = issued.certPem; + data[leafKey(leaf.name)] = issued.keyPem; + changed = true; + log(`tls: issued the ${leaf.name} leaf for ${leaf.hosts.join(", ")} (${leaf.reasons.join("; ")})`); + } + return { data, changed }; +} + +/** The CA bundle of every app container, built as the installer builds it from the configured caBundleFile inputs. */ +export function caBundleOf(config: LibreDBInstallation, caPem: string, readFile: (path: string) => string | undefined): string { + const read = (path: string): CaBundleInput => { + const pem = readFile(path); + if (pem === undefined) throw new Error(`the CA bundle file ${path} does not exist`); + return { source: path, pem }; + }; + const postgresFile = postgresCaBundleFile(config); + const inputs = caBundleFiles(config) + .filter((path) => path !== postgresFile) + .map(read); + return buildCaBundle(caPem, inputs, postgresFile === undefined ? [] : [read(postgresFile)]); +} + +/** The role passwords from the store, by app role. */ +function rolePasswords(config: LibreDBInstallation, store: ObjectData): Partial> { + const roles = expectedRoles({ managedKeycloak: config.keycloak.mode === "managed", backupRole: true }); + return Object.fromEntries(roles.map((role) => [role, required(store, ROLE_SPECS[role].secret)])); +} + +function required(store: ObjectData, name: string): string { + const value = store[name]; + if (value === undefined || value === "") throw new Error(`Secret ${K8S_NAMES.secretStore} holds no ${name}`); + return value; +} + +/** Whether two objects hold the same keys and values. */ +export function sameData(a: ObjectData, b: ObjectData): boolean { + const keys = Object.keys(a); + return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]); +} + +/** Drops the variables an env renderer left unset. */ +function defined(env: Readonly>): ObjectData { + return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)); +} + +/** + * The Caddyfile of a managed Keycloak's edge: the installer's auth site as it renders behind an upstream TLS proxy, here + * the ingress controller, so only /realms// and /resources/ reach Keycloak, and a non-canonical path does not. + */ +export function keycloakEdgeCaddyfile(config: LibreDBInstallation, manifest: ReleaseManifest): string { + const behindIngress: LibreDBInstallation = { + ...config, + tls: { mode: "external", caBundleFile: "/dev/null", external: { httpPort: KEYCLOAK_EDGE_PORT, trustedProxies: ["private_ranges"] } }, + }; + const desired = computeDesired(behindIngress, manifest); + return renderCaddyfile({ ...desired, edge: { ...desired.edge, sites: desired.edge.sites.filter((site) => site.endpoint === "auth") } }); +} + +/** A derived object with the workloads that read it at start, which a change of its content restarts. */ +export interface RenderedObjects { + secrets: { name: string; data: ObjectData; type?: "kubernetes.io/tls"; readers?: Workload[] }[]; + configMaps: { name: string; data: ObjectData; readers?: Workload[] }[]; +} + +/** The keys of Studio's environment that the Studio chart has no value for, as the installer renders them. */ +export const STUDIO_NETWORK_KEYS = ["NO_PROXY", "no_proxy", "HTTPS_PROXY", "NODE_USE_ENV_PROXY"] as const; + +/** Every derived object of the run, from the store, the TLS material and the configuration. */ +export function renderObjects( + input: BootstrapInput, + desired: DesiredInstallation, + store: ObjectData, + sealKeys: readonly SealKeyRecord[], + tls: ObjectData, + io: Pick, +): RenderedObjects { + const { config } = input; + const postgres = { host: desired.postgres.host, port: desired.postgres.port }; + const passwords = rolePasswords(config, store); + const secrets: RenderedObjects["secrets"] = []; + const configMaps: RenderedObjects["configMaps"] = []; + + configMaps.push({ + name: K8S_NAMES.caBundle, + data: { [CA_CERT]: caBundleOf(config, tls[CA_CERT] as string, io.readFile) }, + readers: [WORKLOADS.platform, WORKLOADS.studio, WORKLOADS.keycloak], + }); + + const authSecretPrevious = readAuthSecretPrevious(config.platform, io.readFile); + if (authSecretPrevious !== undefined) io.redactor.register(authSecretPrevious); + const platformEnv = renderPlatformEnv(desired, { + postgres, + secrets: { + authSecret: required(store, APP_SECRET_NAMES.platformAuthSecret), + ...(authSecretPrevious === undefined ? {} : { authSecretPrevious }), + license: readPlatformLicense(config.platform, io.readFile, io.redactor), + oidcClientSecret: required(store, CLIENT_SECRET_NAMES.platform), + databasePassword: passwords.platform as string, + }, + sealPublicKey: activeSealKey(sealKeys, config.bridge.sealKeys.activeKid).publicKey, + }); + secrets.push({ name: K8S_NAMES.platformEnv, data: defined(platformEnv), readers: [WORKLOADS.platform] }); + + const migrateEnv = renderPlatformMigrateEnv(desired, { + postgres, + secrets: { databasePassword: passwords.platform as string, ownerPassword: passwords.owner as string }, + }); + secrets.push({ name: K8S_NAMES.migrateEnv, data: defined(migrateEnv) }); + + const studioEnv = renderStudioEnv(desired, { + postgres, + secrets: { + oidcClientSecret: required(store, CLIENT_SECRET_NAMES.studio), + jwtSecret: required(store, APP_SECRET_NAMES.studioJwtSecret), + storageEncryptionKey: required(store, APP_SECRET_NAMES.studioStorageEncryptionKey), + databasePassword: passwords.studio as string, + }, + }); + secrets.push({ + name: input.studioSecretName, + data: { + [STUDIO_SECRET_KEYS.jwtSecret]: studioEnv.JWT_SECRET as string, + [STUDIO_SECRET_KEYS.oidcClientId]: studioEnv.OIDC_CLIENT_ID as string, + [STUDIO_SECRET_KEYS.oidcClientSecret]: studioEnv.OIDC_CLIENT_SECRET as string, + [STUDIO_SECRET_KEYS.storagePostgresUrl]: studioEnv.STORAGE_POSTGRES_URL as string, + [STUDIO_SECRET_KEYS.governancePostgresUrl]: studioEnv.GOVERNANCE_POSTGRES_URL as string, + [STUDIO_SECRET_KEYS.governancePrivateKeys]: renderKeyring(sealKeys), + [STUDIO_SECRET_KEYS.storageEncryptionKey]: studioEnv.STORAGE_ENCRYPTION_KEY as string, + }, + readers: [WORKLOADS.studio], + }); + secrets.push({ + name: K8S_NAMES.studioNetworkEnv, + data: defined(Object.fromEntries(STUDIO_NETWORK_KEYS.map((key) => [key, studioEnv[key]]))), + readers: [WORKLOADS.studio], + }); + + const managedKeycloak = config.keycloak.mode === "managed"; + if (config.postgres.mode === "managed") { + secrets.push({ name: K8S_NAMES.postgresSuperuser, data: { POSTGRES_PASSWORD: required(store, POSTGRES_SUPERUSER_SECRET) } }); + secrets.push({ + name: K8S_NAMES.postgresTls, + data: { "server.crt": tls[leafCert("postgres")] as string, "server.key": tls[leafKey("postgres")] as string }, + // PostgreSQL reads its leaf at start; a renewed leaf restarts it, which the installer does with a reload. + readers: [WORKLOADS.postgres], + }); + configMaps.push({ name: K8S_NAMES.postgresConfig, data: { "pg_hba.conf": renderPgHba() }, readers: [WORKLOADS.postgres] }); + const roles = expectedRoles({ managedKeycloak, backupRole: true }); + secrets.push({ + name: K8S_NAMES.postgresProvision, + data: { + "provision.sql": renderProvisionScript({ + verifiers: roleVerifiers(passwords), + managedKeycloak, + resetPasswords: roles.filter((role) => role !== "backup"), + }), + }, + }); + } else if (config.postgres.external.provisioning === "manual") { + secrets.push({ + name: K8S_NAMES.postgresDbaScript, + data: { "provision.sql": renderDbaScript({ passwords, managedKeycloak, server: postgres }) }, + }); + } + + // Only the leaves the configuration still wants: after a switch to tls.mode secrets the store keeps the old ones. + const wanted = new Set(desiredTls(config).leaves.map((leaf) => leaf.name)); + for (const endpoint of ["platform", "studio", "auth"] as const) { + const cert = tls[leafCert(endpoint)]; + const key = tls[leafKey(endpoint)]; + if (!wanted.has(endpoint) || cert === undefined || key === undefined) continue; + secrets.push({ + name: K8S_NAMES.ingressTls(endpoint), + type: "kubernetes.io/tls", + data: { "tls.crt": cert, "tls.key": key, [CA_CERT]: tls[CA_CERT] as string }, + }); + } + + if (managedKeycloak) { + configMaps.push({ + name: K8S_NAMES.keycloakEdge, + data: { Caddyfile: keycloakEdgeCaddyfile(config, input.manifest) }, + readers: [WORKLOADS.keycloak], + }); + secrets.push({ + name: K8S_NAMES.keycloakEnv, + data: { + KC_DB_PASSWORD: passwords.keycloak as string, + KC_BOOTSTRAP_ADMIN_CLIENT_ID: BOOTSTRAP_CLIENT, + KC_BOOTSTRAP_ADMIN_CLIENT_SECRET: required(store, STORE_KEYS.keycloakBootstrapSecret), + }, + readers: [WORKLOADS.keycloak], + }); + } + const keycloakConfig: ObjectData = { + [CLIENT_SECRET_NAMES.studio]: required(store, CLIENT_SECRET_NAMES.studio), + [CLIENT_SECRET_NAMES.platform]: required(store, CLIENT_SECRET_NAMES.platform), + }; + if (managedKeycloak) keycloakConfig[STORE_KEYS.keycloakBootstrapSecret] = required(store, STORE_KEYS.keycloakBootstrapSecret); + for (const key of [STORE_KEYS.firstAdminPassword, STORE_KEYS.userManagerPassword]) { + if (store[key] !== undefined) keycloakConfig[key] = store[key]; + } + secrets.push({ name: K8S_NAMES.keycloakConfig, data: keycloakConfig }); + + return { secrets, configMaps }; +} + +/** Runs the bootstrap job. */ +export async function runBootstrap(input: BootstrapInput, io: BootstrapIO): Promise { + const { config, manifest } = input; + if (config.postgres.mode === "external" && config.postgres.external.provisioning !== "manual") { + throw new RefusedError( + "the Helm chart supports an external PostgreSQL server only with provisioning manual: a DBA runs the script in " + + `Secret ${K8S_NAMES.postgresDbaScript}, so no admin credential of the server enters the cluster`, + ExitCode.InvalidUsage, + ); + } + await checkRelease(io.objects, manifest.version); + const state = checkInstallation(await loadState(io.objects), config); + + const converged = convergeStore(config, (await io.objects.getSecret(K8S_NAMES.secretStore)) ?? {}, io); + for (const line of converged.rotated) io.log(`seal keys: ${line}`); + if (converged.changed.length > 0) { + await io.objects.putSecret(K8S_NAMES.secretStore, converged.store); + io.log(`secrets: stored ${converged.changed.join(", ")} in Secret ${K8S_NAMES.secretStore}`); + } + + const tls = await convergeTls(config, (await io.objects.getSecret(K8S_NAMES.tls)) ?? {}, io.now(), io.log); + if (tls.changed) await io.objects.putSecret(K8S_NAMES.tls, tls.data); + + const desired = computeDesired(config, manifest); + const rendered = renderObjects(input, desired, converged.store, converged.sealKeys, tls.data, io); + if (input.jobsBundle !== undefined) { + await io.objects.putConfigMap(K8S_NAMES.jobsBundle, {}, { [JOBS_BUNDLE_KEY]: input.jobsBundle }); + io.log(`wrote ConfigMap ${K8S_NAMES.jobsBundle}`); + } + // A workload reads its Secrets and ConfigMaps when it starts, so one whose input changed must restart. The restarts + // are recorded before any object is written, so a run that fails halfway still leaves them asked for, and the + // restart job carries them out after Helm applied the release. + const restart = new Map(); + const changed = (before: ObjectData | undefined, after: ObjectData, readers: Workload[] | undefined) => { + if (readers === undefined || (before !== undefined && sameData(before, after))) return; + for (const reader of readers) restart.set(`${reader.kind}/${reader.name}`, reader); + }; + for (const configMap of rendered.configMaps) changed(await io.objects.getConfigMap(configMap.name), configMap.data, configMap.readers); + for (const secret of rendered.secrets) changed(await io.objects.getSecret(secret.name), secret.data, secret.readers); + const stateData = (await io.objects.getSecret(K8S_NAMES.state)) ?? {}; + const pending = pendingRestartsOf(stateData); + if (restart.size > 0) { + const asked = addPendingRestarts(pending, [...restart.values()], io.now()); + await io.objects.putSecret(K8S_NAMES.state, { ...stateData, [PENDING_RESTARTS_KEY]: JSON.stringify(asked) }); + io.log(`asked to restart ${asked.workloads.map((workload) => `${workload.kind}/${workload.name}`).join(", ")} once the release is applied`); + } + for (const configMap of rendered.configMaps) { + await io.objects.putConfigMap(configMap.name, configMap.data); + io.log(`wrote ConfigMap ${configMap.name}`); + } + for (const secret of rendered.secrets) { + await io.objects.putSecret(secret.name, secret.data, secret.type); + io.log(`wrote Secret ${secret.name}`); + } + await saveState(io.objects, state); + io.log("bootstrap done"); +} + +/** + * The restart job, a post-install and post-upgrade hook: restarts each workload the bootstrap job asked for, unless all + * its pods were created after the change, as a rollout of the release's own pod template or a first install makes + * them; a workload that does not exist is dropped. The request is cleared only once every restart was done, and the + * release is then recorded as the one the installation runs. + */ +export async function runRestarts( + objects: KubernetesObjects, + version: string, + now: () => Date, + log: (line: string) => void, +): Promise { + const stateData = (await objects.getSecret(K8S_NAMES.state)) ?? {}; + const pending = pendingRestartsOf(stateData); + if (pending === undefined) { + log("no restart asked for"); + await objects.putSecret(K8S_NAMES.state, { ...stateData, [RELEASE_KEY]: version }); + log(`the installation runs LibreDB ${version}`); + return; + } + const since = new Date(pending.since).getTime(); + for (const workload of pending.workloads) { + const name = `${workload.kind}/${workload.name}`; + const created = await objects.podsCreated(workload); + if (created === undefined) { + log(`${name} does not exist`); + } else if (created.length > 0 && created.every((at) => at.getTime() >= since)) { + log(`${name} started after its inputs changed`); + } else { + await objects.restart(workload, now()); + log(`restarted ${name}: an input it reads at start changed`); + } + } + const { [PENDING_RESTARTS_KEY]: _done, ...rest } = (await objects.getSecret(K8S_NAMES.state)) ?? {}; + await objects.putSecret(K8S_NAMES.state, { ...rest, [RELEASE_KEY]: version }); + log(`the installation runs LibreDB ${version}`); +} diff --git a/installer/src/kubernetes/keycloak.ts b/installer/src/kubernetes/keycloak.ts new file mode 100644 index 0000000..f364b67 --- /dev/null +++ b/installer/src/kubernetes/keycloak.ts @@ -0,0 +1,213 @@ +// The keycloak-configure job of the Helm chart: the Kubernetes counterpart of the installer's keycloak-configure step, +// which it runs unchanged. The realm, its clients, client scope, roles and accounts come from desiredKeycloak, the +// actions from planKeycloak, and every Admin REST request from the same request builder, so a Helm install converges +// the same objects as a Docker install. The installer state, which records the accounts it created, lives in a Secret. +// +// A managed Keycloak is reached in-cluster with the bootstrap admin client of realm master, whose secret only the +// generated Secrets hold. Unlike the Docker install, the chart keeps that client: Keycloak's environment is part of a +// Helm-managed Deployment, and Keycloak would recreate the client from it on the next start. +import type { AdminCredentials } from "../adapters/keycloak-admin"; +import { KeycloakAdmin } from "../adapters/keycloak-admin"; +import type { LibreDBInstallation } from "../config/schema"; +import { desiredKeycloak, managedRealm, CLIENT_SECRET_NAMES, type Account } from "../keycloak/desired"; +import { lookupAdminCredentials } from "../keycloak/external"; +import { planKeycloak, type KeycloakAction } from "../keycloak/planner"; +import { MANAGED_ATTRIBUTE } from "../keycloak/representations"; +import type { Redactor } from "../log/redact"; +import { computeDesired } from "../model/desired"; +import type { ReleaseManifest } from "../release/manifest"; +import type { InstallerState } from "../state/store"; +import { ExitCode } from "../cli/exit-codes"; +import { loadState, RefusedError, saveState, type KubernetesObjects } from "./bootstrap"; +import { BOOTSTRAP_CLIENT, K8S_NAMES, KEYCLOAK_SERVICE, STORE_KEYS } from "./names"; + +/** The in-cluster Admin API of a managed Keycloak: its Service on the HTTP port. */ +export const MANAGED_KEYCLOAK_URL = `http://${KEYCLOAK_SERVICE}:8080`; + +/** Where the chart mounts Secret libredb-keycloak-config into the job. */ +export const KEYCLOAK_CONFIG_DIR = "/etc/libredb/keycloak-config"; + +/** The installer step's functions, injected so the pure module does not import the Docker step. */ +export interface ConvergeFunctions { + observe(target: ConvergeTarget, state: InstallerState): Promise[1]>; + apply(actions: readonly KeycloakAction[], target: ConvergeTarget, state: InstallerState): Promise; +} + +/** The ConfigureTarget of the installer's keycloak-configure step. */ +export interface ConvergeTarget { + baseUrl: string; + realm: string; + credentials: AdminCredentials; + desired: ReturnType; + passwordFiles: Readonly>; + owner: { uid: number; gid: number }; + redactor: Redactor; + log: (line: string) => void; + fetch?: typeof fetch; +} + +export interface KeycloakJobIO { + objects: KubernetesObjects; + readFile(path: string): string | undefined; + /** Writes a 0600 file of the job's own user, for the step's temporary password files. */ + writeFile(path: string, content: string): void; + env: Readonly>; + /** A directory the job may write to. */ + tmpDir: string; + owner: { uid: number; gid: number }; + log(line: string): void; + redactor: Redactor; + fetch?: typeof fetch; + sleep?: (ms: number) => Promise; + now?: () => number; +} + +function configValue(io: Pick, key: string): string { + const value = io.readFile(`${KEYCLOAK_CONFIG_DIR}/${key}`); + if (value === undefined || value === "") throw new Error(`Secret ${K8S_NAMES.keycloakConfig} holds no ${key}`); + io.redactor.register(value); + return value; +} + +/** Where the job reaches the Admin API and as whom. */ +export function adminTarget( + config: LibreDBInstallation, + io: Pick, +): { baseUrl: string; credentials: AdminCredentials } { + if (config.keycloak.mode === "managed") { + return { + baseUrl: MANAGED_KEYCLOAK_URL, + credentials: { realm: "master", clientId: BOOTSTRAP_CLIENT, clientSecret: configValue(io, STORE_KEYS.keycloakBootstrapSecret) }, + }; + } + const { external } = config.keycloak; + if (external.provisioning === "manual") { + throw new RefusedError( + "keycloak.external.provisioning is manual: the site configures the realm, and the chart runs no keycloak job", + ExitCode.InvalidUsage, + ); + } + const lookup = lookupAdminCredentials(external, { env: io.env, readFile: io.readFile }); + if ("missing" in lookup) throw new RefusedError(lookup.missing, ExitCode.InvalidUsage); + io.redactor.register(lookup.secret); + return { baseUrl: external.adminUrl ?? external.baseUrl, credentials: lookup.credentials }; +} + +/** Creates the realm of a managed Keycloak with the settings the converge keeps, as the installer's handover does. */ +export async function ensureManagedRealm(admin: Pick, realm: string): Promise<"created" | "exists"> { + const found = await admin.find<{ attributes?: Record }>(`/admin/realms/${encodeURIComponent(realm)}`); + if (found === undefined) { + await admin.write("POST", "/admin/realms", managedRealm(realm), [201]); + return "created"; + } + if (found.attributes?.[MANAGED_ATTRIBUTE] !== "true") { + throw new RefusedError( + `realm ${realm} exists without the attribute ${MANAGED_ATTRIBUTE}=true that the installer gives the realm it ` + + "creates, so the chart does not take it over; install with keycloak.mode external instead", + ExitCode.UnsafeChangeRefused, + ); + } + return "exists"; +} + +/** How long the job waits for Keycloak to answer before it fails, and how often it tries. */ +export const KEYCLOAK_WAIT = { timeoutMs: 10 * 60_000, intervalMs: 3_000 } as const; + +/** + * Waits until Keycloak answers HTTP at `baseUrl`, whatever the status, so a job that starts beside a Keycloak that is + * still starting waits instead of failing; it fails with the last connection error once `timeoutMs` passed. + */ +export async function waitForKeycloak( + baseUrl: string, + options: { + fetch: typeof fetch; + log: (line: string) => void; + sleep: (ms: number) => Promise; + now: () => number; + timeoutMs?: number; + intervalMs?: number; + }, +): Promise { + const { timeoutMs = KEYCLOAK_WAIT.timeoutMs, intervalMs = KEYCLOAK_WAIT.intervalMs } = options; + const deadline = options.now() + timeoutMs; + let waiting = false; + for (;;) { + try { + const response = await options.fetch(`${baseUrl}/realms/master`, { signal: AbortSignal.timeout(intervalMs * 3) }); + await response.body?.cancel(); + if (waiting) options.log(`keycloak: ${baseUrl} answers`); + return; + } catch (error) { + if (options.now() >= deadline) { + throw new Error(`Keycloak at ${baseUrl} did not answer within ${timeoutMs / 1000} s: ${(error as Error).message}`); + } + if (!waiting) options.log(`keycloak: waiting for ${baseUrl} to answer`); + waiting = true; + await options.sleep(intervalMs); + } + } +} + +/** Runs the keycloak-configure job. */ +export async function runKeycloakJob( + config: LibreDBInstallation, + manifest: ReleaseManifest, + io: KeycloakJobIO, + converge: ConvergeFunctions, +): Promise { + const desired = computeDesired(config, manifest); + const { baseUrl, credentials } = adminTarget(config, io); + await waitForKeycloak(baseUrl, { + fetch: io.fetch ?? fetch, + log: io.log, + sleep: io.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))), + now: io.now ?? Date.now, + }); + if (config.keycloak.mode === "managed") { + const admin = new KeycloakAdmin({ baseUrl, credentials, log: io.log, ...(io.fetch === undefined ? {} : { fetch: io.fetch }) }); + const realm = await ensureManagedRealm(admin, desired.realm); + io.log(`keycloak: realm ${desired.realm} ${realm}`); + } + const passwordFiles: Record = { + "first-admin": `${io.tmpDir}/first-admin.txt`, + "user-manager": `${io.tmpDir}/user-manager.txt`, + }; + // The temporary passwords were generated by the bootstrap job; the step reads a stored password file before it + // generates one, so the accounts get exactly these. + const accounts: [Account, boolean, string][] = [ + ["first-admin", config.keycloak.firstAdmin.enabled, STORE_KEYS.firstAdminPassword], + ["user-manager", config.keycloak.userManager.enabled, STORE_KEYS.userManagerPassword], + ]; + for (const [account, enabled, key] of accounts) { + if (enabled) io.writeFile(passwordFiles[account], `${configValue(io, key)}\n`); + } + const target: ConvergeTarget = { + baseUrl, + realm: desired.realm, + credentials, + desired: desiredKeycloak({ + installation: desired, + keycloak: config.keycloak, + clientSecrets: { + studio: configValue(io, CLIENT_SECRET_NAMES.studio), + platform: configValue(io, CLIENT_SECRET_NAMES.platform), + }, + passwordLocations: { + firstAdmin: `Secret ${K8S_NAMES.keycloakConfig} key ${STORE_KEYS.firstAdminPassword}`, + userManager: `Secret ${K8S_NAMES.keycloakConfig} key ${STORE_KEYS.userManagerPassword}`, + }, + }), + passwordFiles, + owner: io.owner, + redactor: io.redactor, + log: io.log, + ...(io.fetch === undefined ? {} : { fetch: io.fetch }), + }; + const state = await loadState(io.objects); + const observed = await converge.observe(target, state); + const actions = planKeycloak(target.desired, observed); + for (const action of actions) io.log(`keycloak: ${action.summary}`); + await converge.apply(actions, target, state); + await saveState(io.objects, state); + io.log(actions.length === 0 ? "keycloak: realm already converged" : `keycloak: applied ${actions.length} actions`); +} diff --git a/installer/src/kubernetes/names.ts b/installer/src/kubernetes/names.ts new file mode 100644 index 0000000..efd9caf --- /dev/null +++ b/installer/src/kubernetes/names.ts @@ -0,0 +1,91 @@ +// The names of the Kubernetes objects of an installation that the Helm chart charts/libredb-platform deploys. The +// chart's templates name the same objects; a chart test renders them and compares, so the two never drift. +// Every name is fixed, like the installer's Kubernetes constants (Service libredb-postgres), so a namespace holds one +// installation. +import { POSTGRES_KUBERNETES_SERVICE } from "../tls/plan"; + +/** The Service of a managed Keycloak; Platform's KEYCLOAK_ADMIN_URL of a managed Keycloak is http://keycloak:8080. */ +export const KEYCLOAK_SERVICE = "keycloak"; + +export const K8S_NAMES = { + /** Generated secrets and seal keys, never overwritten once generated. */ + secretStore: "libredb-secrets", + /** The installer state (installer/src/state/store.ts), which holds no secret value. */ + state: "libredb-installer-state", + /** The internal CA and the leaves it issued, as PEM. */ + tls: "libredb-tls", + /** The CA bundle every app container mounts at /etc/libredb/ca/ca.crt. */ + caBundle: "libredb-ca", + /** Platform's runtime environment, for `serve` only. */ + platformEnv: "libredb-platform-env", + /** The migrate job's environment, the only place MIGRATE_DATABASE_URL lives. */ + migrateEnv: "libredb-platform-migrate-env", + /** The managed PostgreSQL server: its Service, which the leaf covers, and its objects. */ + postgres: POSTGRES_KUBERNETES_SERVICE, + /** The superuser password of the managed PostgreSQL server, as POSTGRES_PASSWORD. */ + postgresSuperuser: "libredb-postgres-superuser", + /** The managed server's leaf and key. */ + postgresTls: "libredb-postgres-tls", + /** pg_hba.conf of the managed server. */ + postgresConfig: "libredb-postgres-config", + /** The provisioning script of a managed server, with SCRAM verifiers only. */ + postgresProvision: "libredb-postgres-provision", + /** The script a DBA runs on an external server with provisioning manual, with SCRAM verifiers only. */ + postgresDbaScript: "libredb-postgres-dba-script", + /** A managed Keycloak's database password and bootstrap admin client. */ + keycloakEnv: "libredb-keycloak-env", + /** The Caddyfile of a managed Keycloak's edge, the installer's auth site behind the ingress. */ + keycloakEdge: "libredb-keycloak-edge", + /** Studio's network environment of the installer (NO_PROXY, and HTTPS_PROXY with a proxy), read through studio.extraEnvFrom. */ + studioNetworkEnv: "libredb-studio-network-env", + /** The jobs' gzipped bundle, which the bootstrap job copies from its hook ConfigMap for the keycloak-configure job. */ + jobsBundle: "libredb-jobs", + /** What the keycloak-configure job needs: the client secrets and the temporary passwords. */ + keycloakConfig: "libredb-keycloak-config", + /** The ingress leaves of tls.mode generated, one kubernetes.io/tls Secret per endpoint. */ + ingressTls: (endpoint: "platform" | "studio" | "auth") => `libredb-tls-${endpoint}`, +} as const; + +/** The plain HTTP port of a managed Keycloak's edge, which the ingress forwards hosts.auth to. */ +export const KEYCLOAK_EDGE_PORT = 8081; + +/** The bootstrap admin client of a managed Keycloak in realm master, as the installer's handover names it. */ +export const BOOTSTRAP_CLIENT = "libredb-bootstrap"; + +/** The secret store's names besides the installer's generated secrets. */ +export const STORE_KEYS = { + sealKeys: "seal-keys.json", + keycloakBootstrapSecret: "keycloak-bootstrap-client-secret", + firstAdminPassword: "first-admin-password", + userManagerPassword: "user-manager-password", +} as const; + +/** The workloads the bootstrap job restarts when an object they read at start changed. */ +export const WORKLOADS = { + platform: { kind: "deployments", name: "libredb-platform" }, + studio: { kind: "deployments", name: "libredb-studio" }, + keycloak: { kind: "deployments", name: KEYCLOAK_SERVICE }, + postgres: { kind: "statefulsets", name: POSTGRES_KUBERNETES_SERVICE }, +} as const; + +export type Workload = (typeof WORKLOADS)[keyof typeof WORKLOADS]; + +/** The label of every object the jobs write, which names the installation: never a Helm or Argo CD tracking label. */ +export const INSTALLATION_LABEL = "install.libredb.org/installation"; + +/** The key of the gzipped bundle in its ConfigMaps. */ +export const JOBS_BUNDLE_KEY = "libredb-jobs.cjs.gz"; + +/** Where the chart mounts the operator's inputs into the bootstrap job. */ +export const INPUT_DIR = "/etc/libredb/inputs"; + +/** The keys of the Studio Secret, which the Studio chart reads as secrets.existingSecret. */ +export const STUDIO_SECRET_KEYS = { + jwtSecret: "jwt-secret", + oidcClientId: "oidc-client-id", + oidcClientSecret: "oidc-client-secret", + storagePostgresUrl: "storage-postgres-url", + governancePostgresUrl: "governance-postgres-url", + governancePrivateKeys: "governance-private-keys", + storageEncryptionKey: "storage-encryption-key", +} as const; diff --git a/installer/test/unit/adapters/kubernetes-api.test.ts b/installer/test/unit/adapters/kubernetes-api.test.ts new file mode 100644 index 0000000..8c1462b --- /dev/null +++ b/installer/test/unit/adapters/kubernetes-api.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, test } from "bun:test"; +import { KEEP_ANNOTATIONS, KubernetesApi, KubernetesApiError, RESTARTED_AT, SERVICE_ACCOUNT_DIR } from "../../../src/adapters/kubernetes-api"; + +interface Call { + method: string; + url: string; + body?: { + metadata: { name: string; resourceVersion?: string; labels?: Record; annotations?: Record }; + data?: Record; + type?: string; + }; +} + +/** An API server with one namespace's Secrets, answering as kube-apiserver does. */ +function apiServer(initial: Record; version: string; type?: string; labels?: Record }> = {}) { + const objects = new Map(Object.entries(initial)); + const calls: Call[] = []; + let version = 100; + const fetcher = (async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input); + const method = init?.method ?? "GET"; + const body = init?.body === undefined ? undefined : (JSON.parse(String(init.body)) as Call["body"]); + calls.push({ method, url, ...(body === undefined ? {} : { body }) }); + const name = url.split("/").pop() as string; + if (method === "GET") { + const found = objects.get(name); + if (found === undefined) return Response.json({ message: "not found" }, { status: 404 }); + return Response.json({ metadata: { name, resourceVersion: found.version, labels: found.labels }, data: found.data, type: found.type }); + } + const target = body?.metadata.name as string; + if (method === "PUT" && objects.get(target)?.version !== body?.metadata.resourceVersion) { + return Response.json({ message: "the object has been modified" }, { status: 409 }); + } + version += 1; + objects.set(target, { data: body?.data ?? {}, version: String(version) }); + return Response.json({ metadata: { name: target, resourceVersion: String(version) } }, { status: method === "POST" ? 201 : 200 }); + }) as typeof fetch; + return { fetcher, calls, objects }; +} + +const b64 = (text: string) => Buffer.from(text).toString("base64"); + +function api(fetcher: typeof fetch) { + return new KubernetesApi({ server: "https://10.0.0.1:443", token: "t", namespace: "libredb", labels: { "app.kubernetes.io/part-of": "libredb" }, fetch: fetcher }); +} + +describe("KubernetesApi", () => { + test("reads a Secret decoded, and undefined for a missing one", async () => { + const server = apiServer({ present: { data: { key: b64("value") }, version: "7" } }); + const kube = api(server.fetcher); + expect(await kube.getSecret("present")).toEqual({ key: "value" }); + expect(await kube.getSecret("missing")).toBeUndefined(); + expect(server.calls[0]?.url).toBe("https://10.0.0.1:443/api/v1/namespaces/libredb/secrets/present"); + }); + + test("creates a missing Secret with the job's labels, and replaces it with the version it read", async () => { + const server = apiServer(); + const kube = api(server.fetcher); + await kube.putSecret("store", { a: "1" }); + await kube.putSecret("store", { a: "2" }, "Opaque"); + const writes = server.calls.filter((call) => call.method !== "GET"); + expect(writes.map((call) => call.method)).toEqual(["POST", "PUT"]); + expect(writes[0]?.body?.metadata).toEqual({ name: "store", labels: { "app.kubernetes.io/part-of": "libredb" }, annotations: { ...KEEP_ANNOTATIONS } }); + expect(writes[1]?.body?.metadata.resourceVersion).toBe("101"); + expect(Buffer.from(server.objects.get("store")?.data.a as string, "base64").toString()).toBe("2"); + }); + + test("a replace after another writer changed the object fails instead of overwriting it", async () => { + const server = apiServer({ store: { data: {}, version: "1" } }); + const kube = api(server.fetcher); + await kube.getSecret("store"); + server.objects.set("store", { data: {}, version: "2" }); + await expect(kube.putSecret("store", { a: "1" })).rejects.toThrow(KubernetesApiError); + }); + + test("writes a ConfigMap's data as plain text", async () => { + const server = apiServer(); + await api(server.fetcher).putConfigMap("libredb-ca", { "ca.crt": "pem" }); + const write = server.calls.find((call) => call.method === "POST"); + expect(write?.url).toBe("https://10.0.0.1:443/api/v1/namespaces/libredb/configmaps"); + expect(write?.body?.data).toEqual({ "ca.crt": "pem" }); + }); + + test("a failed read names the status and the server's reason", async () => { + const fetcher = (async () => Response.json({ message: "forbidden by RBAC" }, { status: 403 })) as unknown as typeof fetch; + await expect(api(fetcher).getSecret("x")).rejects.toThrow("answered 403: forbidden by RBAC"); + }); + + test("inCluster reads the service account and the API server's address", () => { + const files: Record = { [`${SERVICE_ACCOUNT_DIR}/token`]: "tok\n", [`${SERVICE_ACCOUNT_DIR}/namespace`]: "libredb\n" }; + const kube = KubernetesApi.inCluster({ KUBERNETES_SERVICE_HOST: "fd00::1", KUBERNETES_SERVICE_PORT: "6443" }, (path) => files[path], {}); + expect(kube.namespace).toBe("libredb"); + expect(() => KubernetesApi.inCluster({}, () => undefined, {})).toThrow("not running in a Kubernetes pod"); + }); +}); + +describe("KubernetesApi beyond Secrets", () => { + test("a replace keeps the labels others gave the object, except Helm's tracking label", async () => { + const server = apiServer({ store: { data: {}, version: "3", labels: { team: "db", "app.kubernetes.io/instance": "libredb" } } }); + await api(server.fetcher).putSecret("store", { a: "1" }); + const put = server.calls.find((call) => call.method === "PUT"); + expect(put?.body?.metadata.labels).toEqual({ team: "db", "app.kubernetes.io/part-of": "libredb" }); + }); + + test("a Secret of another type in the way fails naming it instead of a bare 422", async () => { + const server = apiServer({ "libredb-tls-platform": { data: {}, version: "1", type: "Opaque" } }); + await expect(api(server.fetcher).putSecret("libredb-tls-platform", { a: "1" }, "kubernetes.io/tls")).rejects.toThrow( + "exists with type Opaque, not kubernetes.io/tls", + ); + }); + + test("reads a ConfigMap's data, and undefined for a missing one", async () => { + const fetcher = (async (input: string | URL | Request) => + String(input).endsWith("/present") ? Response.json({ metadata: { name: "present" }, data: { k: "v" } }) : Response.json({}, { status: 404 })) as unknown as typeof fetch; + expect(await api(fetcher).getConfigMap("present")).toEqual({ k: "v" }); + expect(await api(fetcher).getConfigMap("missing")).toBeUndefined(); + }); + + test("restarts a workload with a merge patch of its pod template, and reports one that does not exist", async () => { + const calls: { url: string; method?: string; type?: string | null; body?: string }[] = []; + const fetcher = (async (input: string | URL | Request, init?: RequestInit) => { + calls.push({ url: String(input), method: init?.method, type: new Headers(init?.headers).get("content-type"), body: String(init?.body) }); + return String(input).includes("/statefulsets/") ? Response.json({}, { status: 404 }) : Response.json({}); + }) as unknown as typeof fetch; + const at = new Date("2026-10-20T00:00:00Z"); + expect(await api(fetcher).restart({ kind: "deployments", name: "libredb-platform" }, at)).toBe("restarted"); + expect(await api(fetcher).restart({ kind: "statefulsets", name: "libredb-postgres" }, at)).toBe("absent"); + expect(calls[0]).toEqual({ + url: "https://10.0.0.1:443/apis/apps/v1/namespaces/libredb/deployments/libredb-platform", + method: "PATCH", + type: "application/merge-patch+json", + body: JSON.stringify({ spec: { template: { metadata: { annotations: { [RESTARTED_AT]: at.toISOString() } } } } }), + }); + const refused = (async () => Response.json({ message: "forbidden" }, { status: 403 })) as unknown as typeof fetch; + await expect(api(refused).restart({ kind: "deployments", name: "keycloak" }, at)).rejects.toThrow("answered 403: forbidden"); + }); +}); + +describe("KubernetesApi.podsCreated", () => { + test("lists the creation times of a workload's pods by its selector, and undefined for a missing workload", async () => { + const urls: string[] = []; + const fetcher = (async (input: string | URL | Request) => { + const url = String(input); + urls.push(url); + if (url.endsWith("/deployments/keycloak")) return Response.json({}, { status: 404 }); + if (url.includes("/deployments/")) return Response.json({ spec: { selector: { matchLabels: { app: "platform", tier: "web" } } } }); + return Response.json({ items: [{ metadata: { creationTimestamp: "2026-10-20T00:00:00Z" } }] }); + }) as unknown as typeof fetch; + const created = await api(fetcher).podsCreated({ kind: "deployments", name: "libredb-platform" }); + expect(created).toEqual([new Date("2026-10-20T00:00:00Z")]); + expect(urls[1]).toBe("https://10.0.0.1:443/api/v1/namespaces/libredb/pods?labelSelector=app%3Dplatform%2Ctier%3Dweb"); + expect(await api(fetcher).podsCreated({ kind: "deployments", name: "keycloak" })).toBeUndefined(); + }); + + test("a refused read names the status", async () => { + let calls = 0; + const fetcher = (async () => { + calls += 1; + return calls === 1 ? Response.json({ message: "no" }, { status: 403 }) : Response.json({}); + }) as unknown as typeof fetch; + await expect(api(fetcher).podsCreated({ kind: "deployments", name: "libredb-studio" })).rejects.toThrow("answered 403"); + const pods = (async (input: string | URL | Request) => + String(input).includes("/pods") ? Response.json({ message: "no pods" }, { status: 403 }) : Response.json({ spec: { selector: { matchLabels: { a: "b" } } } })) as unknown as typeof fetch; + await expect(api(pods).podsCreated({ kind: "deployments", name: "libredb-studio" })).rejects.toThrow("no pods"); + }); +}); diff --git a/installer/test/unit/kubernetes/bootstrap.test.ts b/installer/test/unit/kubernetes/bootstrap.test.ts new file mode 100644 index 0000000..52cb0ef --- /dev/null +++ b/installer/test/unit/kubernetes/bootstrap.test.ts @@ -0,0 +1,478 @@ +import { describe, expect, test } from "bun:test"; +import { PLATFORM_ENV_KEYS, PLATFORM_MIGRATE_ENV_KEYS } from "../../../src/contracts/platform"; +import { APP_SECRET_NAMES, POSTGRES_SUPERUSER_SECRET } from "../../../src/docker/secrets"; +import { CLIENT_SECRET_NAMES } from "../../../src/keycloak/desired"; +import { + addPendingRestarts, + caBundleOf, + checkRelease, + pendingRestartsOf, + runRestarts, + RELEASE_KEY, + sameData, + checkInstallation, + convergeStore, + convergeTls, + generatedNames, + keycloakEdgeCaddyfile, + loadState, + observedTls, + runBootstrap, + saveState, + rotateSealKeys, + SEAL_ENVELOPES_FILE, + sealKeysOf, + STATE_KEY, +} from "../../../src/kubernetes/bootstrap"; +import { INPUT_DIR, K8S_NAMES, STORE_KEYS, STUDIO_SECRET_KEYS, WORKLOADS } from "../../../src/kubernetes/names"; +import { Redactor } from "../../../src/log/redact"; +import { emptyState } from "../../../src/state/store"; +import { UnsafeChangeError } from "../../../src/state/immutable"; +import { createCa } from "../../../src/tls/ca"; +import { manifestFixture } from "../release/fixture"; +import { EXTERNAL_POSTGRES, files, installation, INSTALLER_KEYCLOAK, io, LICENSE, MANUAL_KEYCLOAK, MemoryObjects } from "./fixture"; + +const STUDIO_SECRET = "libredb-studio-env"; + +async function bootstrap(objects: MemoryObjects, config = installation(), readFile = files(), now?: Date) { + const run = io(objects, readFile, now); + await runBootstrap({ config, manifest: manifestFixture(), studioSecretName: STUDIO_SECRET }, run.io); + return run.lines; +} + +describe("the bootstrap job with a managed PostgreSQL and Keycloak", () => { + test("writes every Secret and ConfigMap the chart's workloads mount", async () => { + const objects = new MemoryObjects(); + const lines = await bootstrap(objects); + expect([...objects.secrets.keys()].sort()).toEqual( + [ + K8S_NAMES.secretStore, + K8S_NAMES.state, + K8S_NAMES.tls, + K8S_NAMES.platformEnv, + K8S_NAMES.migrateEnv, + STUDIO_SECRET, + K8S_NAMES.studioNetworkEnv, + K8S_NAMES.postgresSuperuser, + K8S_NAMES.postgresTls, + K8S_NAMES.postgresProvision, + K8S_NAMES.keycloakEnv, + K8S_NAMES.keycloakConfig, + K8S_NAMES.ingressTls("platform"), + K8S_NAMES.ingressTls("studio"), + K8S_NAMES.ingressTls("auth"), + ].sort(), + ); + expect([...objects.configMaps.keys()].sort()).toEqual( + [K8S_NAMES.caBundle, K8S_NAMES.postgresConfig, K8S_NAMES.keycloakEdge].sort(), + ); + expect(objects.secrets.get(K8S_NAMES.ingressTls("platform"))?.type).toBe("kubernetes.io/tls"); + expect(lines.at(-1)).toBe("bootstrap done"); + expect(lines.join("\n")).not.toContain(objects.secret(K8S_NAMES.secretStore)[APP_SECRET_NAMES.platformAuthSecret] as string); + }); + + test("Platform's environment holds exactly the installer's env set, never MIGRATE_DATABASE_URL", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const env = objects.secret(K8S_NAMES.platformEnv); + for (const key of Object.keys(env)) expect(PLATFORM_ENV_KEYS as readonly string[]).toContain(key); + expect(env.MIGRATE_DATABASE_URL).toBeUndefined(); + expect(env.PLATFORM_LICENSE).toBe(LICENSE); + expect(env.AUTH_URL).toBe("https://platform.libredb.test"); + expect(env.AUTH_KEYCLOAK_ISSUER).toBe("https://auth.libredb.test/realms/libredb"); + expect(env.KEYCLOAK_ADMIN_URL).toBe("http://keycloak:8080"); + expect(env.DATABASE_URL).toStartWith("postgresql://libredb_platform:"); + expect(env.DATABASE_URL).toContain("@libredb-postgres:5432/libredb?schema=platform&sslmode=verify-full"); + const migrate = objects.secret(K8S_NAMES.migrateEnv); + for (const key of Object.keys(migrate)) expect(PLATFORM_MIGRATE_ENV_KEYS as readonly string[]).toContain(key); + expect(migrate.MIGRATE_DATABASE_URL).toStartWith("postgresql://libredb_owner:"); + }); + + test("Studio's Secret holds the keys of the Studio chart, a keyring of the live seal key and the studio role's URL", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const studio = objects.secret(STUDIO_SECRET); + expect(Object.keys(studio).sort()).toEqual(Object.values(STUDIO_SECRET_KEYS).sort()); + expect(studio[STUDIO_SECRET_KEYS.oidcClientId]).toBe("libredb-studio"); + expect(studio[STUDIO_SECRET_KEYS.governancePostgresUrl]).toStartWith("postgresql://libredb_studio:"); + const keyring = JSON.parse(studio[STUDIO_SECRET_KEYS.governancePrivateKeys] as string) as { version: number; keys: string[] }; + const seal = JSON.parse(objects.secret(K8S_NAMES.secretStore)[STORE_KEYS.sealKeys] as string) as { privateKey: string; publicKey: string }[]; + expect(keyring).toEqual({ version: 1, keys: [seal[0]?.privateKey as string] }); + // Platform holds the public half only. + expect(objects.secret(K8S_NAMES.platformEnv).BRIDGE_SEAL_PUBLIC_KEY).toBe(seal[0]?.publicKey as string); + expect(JSON.stringify(objects.secret(K8S_NAMES.platformEnv))).not.toContain(seal[0]?.privateKey as string); + }); + + test("the managed server gets the installer's pg_hba.conf, a leaf for its Service names and a script with verifiers only", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + expect(objects.configMaps.get(K8S_NAMES.postgresConfig)?.["pg_hba.conf"]).toContain("hostssl all all 0.0.0.0/0 scram-sha-256"); + const store = objects.secret(K8S_NAMES.secretStore); + const script = objects.secret(K8S_NAMES.postgresProvision)["provision.sql"] as string; + expect(script).toContain("SCRAM-SHA-256$"); + expect(script).not.toContain(store["postgres-platform-password"] as string); + expect(objects.secret(K8S_NAMES.postgresSuperuser).POSTGRES_PASSWORD).toBe(store[POSTGRES_SUPERUSER_SECRET] as string); + expect(objects.secret(K8S_NAMES.postgresTls)["server.crt"]).toContain("BEGIN CERTIFICATE"); + }); + + test("the managed Keycloak gets its database password, the bootstrap client and the installer's auth edge", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const store = objects.secret(K8S_NAMES.secretStore); + expect(objects.secret(K8S_NAMES.keycloakEnv)).toEqual({ + KC_DB_PASSWORD: store["postgres-keycloak-password"] as string, + KC_BOOTSTRAP_ADMIN_CLIENT_ID: "libredb-bootstrap", + KC_BOOTSTRAP_ADMIN_CLIENT_SECRET: store[STORE_KEYS.keycloakBootstrapSecret] as string, + }); + const config = objects.secret(K8S_NAMES.keycloakConfig); + expect(Object.keys(config).sort()).toEqual( + [CLIENT_SECRET_NAMES.studio, CLIENT_SECRET_NAMES.platform, STORE_KEYS.keycloakBootstrapSecret, STORE_KEYS.firstAdminPassword, STORE_KEYS.userManagerPassword].sort(), + ); + const caddyfile = objects.configMaps.get(K8S_NAMES.keycloakEdge)?.Caddyfile as string; + expect(caddyfile).toContain("http://auth.libredb.test:8081 {"); + expect(caddyfile).toContain("@keycloak path /realms/libredb/* /resources/*"); + expect(caddyfile).not.toContain("platform.libredb.test"); + }); + + test("a second run keeps every generated secret, the seal keys and the internal CA, and rewrites only derived objects", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const store = objects.secret(K8S_NAMES.secretStore); + const tls = objects.secret(K8S_NAMES.tls); + objects.writes.length = 0; + const lines = await bootstrap(objects); + expect(objects.secret(K8S_NAMES.secretStore)).toEqual(store); + expect(objects.secret(K8S_NAMES.tls)).toEqual(tls); + expect(objects.writes).not.toContain(`secret/${K8S_NAMES.secretStore}`); + expect(objects.writes).not.toContain(`secret/${K8S_NAMES.tls}`); + expect(lines.join("\n")).not.toContain("secrets: stored"); + }); + + test("refuses a run that changes an immutable field, such as postgres.mode", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const external = installation(EXTERNAL_POSTGRES); + await expect(bootstrap(objects, external, files({ [`${INPUT_DIR}/ca/postgres-ca.crt`]: (await createCa({ commonName: "corp", now: new Date() })).certPem }))).rejects.toThrow(UnsafeChangeError); + }); + + test("an AUTH_SECRET rotation passes the previous secret to Platform", async () => { + const objects = new MemoryObjects(); + const previousFile = `${INPUT_DIR}/auth-secret-previous`; + const config = installation({ platform: { organizationName: "Acme", licenseFile: `${INPUT_DIR}/license/license`, authSecretPreviousFile: previousFile } }); + await bootstrap(objects, config, files({ [previousFile]: "the-previous-auth-secret-of-32-characters" })); + expect(objects.secret(K8S_NAMES.platformEnv).AUTH_SECRET_PREVIOUS).toBe("the-previous-auth-secret-of-32-characters"); + }); + + test("refuses a missing license file", async () => { + const objects = new MemoryObjects(); + await expect(bootstrap(objects, installation(), () => undefined)).rejects.toThrow("platform.licenseFile"); + }); +}); + +describe("the bootstrap job with an external PostgreSQL and the site's Keycloak", () => { + test("writes the DBA's provisioning script, puts the server's CA first in the bundle and takes the site's client secrets", async () => { + const objects = new MemoryObjects(); + const postgresCa = await createCa({ commonName: "Corp PostgreSQL CA", now: new Date() }); + const config = installation({ ...EXTERNAL_POSTGRES, ...MANUAL_KEYCLOAK }); + await bootstrap( + objects, + config, + files({ + [`${INPUT_DIR}/ca/postgres-ca.crt`]: postgresCa.certPem, + [`${INPUT_DIR}/keycloak-clients/studio-client-secret`]: "corp-studio-secret\n", + [`${INPUT_DIR}/keycloak-clients/platform-client-secret`]: "corp-platform-secret\n", + }), + ); + expect(objects.secrets.has(K8S_NAMES.postgresProvision)).toBe(false); + expect(objects.secrets.has(K8S_NAMES.postgresSuperuser)).toBe(false); + expect(objects.secrets.has(K8S_NAMES.keycloakEnv)).toBe(false); + expect(objects.configMaps.has(K8S_NAMES.keycloakEdge)).toBe(false); + expect(objects.secrets.has(K8S_NAMES.ingressTls("auth"))).toBe(false); + const script = objects.secret(K8S_NAMES.postgresDbaScript)["provision.sql"] as string; + expect(script).toContain("pg.corp.example:5432"); + expect(script).not.toContain("libredb_keycloak"); + const bundle = objects.configMaps.get(K8S_NAMES.caBundle)?.["ca.crt"] as string; + // Prisma trusts only the first certificate of sslcert, so the external server's CA leads the bundle. + expect(bundle.split("\n")[1]).toBe(postgresCa.certPem.split("\n")[1] as string); + expect(objects.secret(STUDIO_SECRET)[STUDIO_SECRET_KEYS.oidcClientSecret]).toBe("corp-studio-secret"); + expect(objects.secret(K8S_NAMES.platformEnv).AUTH_KEYCLOAK_SECRET).toBe("corp-platform-secret"); + expect(objects.secret(K8S_NAMES.platformEnv).DATABASE_URL).toContain("@pg.corp.example:5432/"); + }); + + test("a changed site client secret replaces the stored one on the next run", async () => { + const objects = new MemoryObjects(); + const config = installation(MANUAL_KEYCLOAK); + const secrets = (studio: string) => + files({ + [`${INPUT_DIR}/keycloak-clients/studio-client-secret`]: studio, + [`${INPUT_DIR}/keycloak-clients/platform-client-secret`]: "corp-platform-secret", + }); + await bootstrap(objects, config, secrets("first")); + await bootstrap(objects, config, secrets("second")); + expect(objects.secret(K8S_NAMES.secretStore)[CLIENT_SECRET_NAMES.studio]).toBe("second"); + }); + + test("refuses a site client secret that is not mounted", async () => { + await expect(bootstrap(new MemoryObjects(), installation(MANUAL_KEYCLOAK))).rejects.toThrow("keycloak.external.studioClientSecret"); + }); + + test("refuses an external server with provisioning installer, which would bring an admin credential into the cluster", async () => { + const config = installation({ + postgres: { mode: "external", external: { host: "pg.corp.example", adminUser: "postgres", adminPasswordEnv: "PG_ADMIN" } }, + }); + await expect(bootstrap(new MemoryObjects(), config)).rejects.toMatchObject({ exitCode: 2, message: expect.stringContaining("provisioning manual") }); + }); + + test("an external Keycloak with provisioning installer gets generated client secrets and no accounts", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects, installation(INSTALLER_KEYCLOAK)); + const config = objects.secret(K8S_NAMES.keycloakConfig); + expect(Object.keys(config).sort()).toEqual([CLIENT_SECRET_NAMES.studio, CLIENT_SECRET_NAMES.platform].sort()); + expect(objects.secret(K8S_NAMES.platformEnv).KEYCLOAK_ADMIN_URL).toBe("https://kc-admin.corp.example"); + }); +}); + +describe("the bootstrap job's parts", () => { + test("generatedNames adds the chart's own secrets to the installer's set", () => { + expect(generatedNames(installation())).toEqual( + expect.arrayContaining([STORE_KEYS.keycloakBootstrapSecret, STORE_KEYS.firstAdminPassword, STORE_KEYS.userManagerPassword]), + ); + const site = generatedNames(installation(MANUAL_KEYCLOAK)); + expect(site).not.toContain(STORE_KEYS.keycloakBootstrapSecret); + expect(site).not.toContain(STORE_KEYS.firstAdminPassword); + expect(site).not.toContain(CLIENT_SECRET_NAMES.studio); + }); + + test("sealKeysOf issues the first key once and refuses an emptied store", () => { + const first = sealKeysOf(undefined); + expect(first.issued).toBe(true); + expect(first.keys).toHaveLength(1); + expect(sealKeysOf(JSON.stringify(first.keys))).toEqual({ keys: first.keys, issued: false }); + expect(() => sealKeysOf("[]")).toThrow("holds no seal key"); + expect(() => sealKeysOf("{}")).toThrow("holds no seal key"); + }); + + test("convergeStore keeps a stored value and registers every value with the redactor", () => { + const redactor = new Redactor(); + const stored = { [APP_SECRET_NAMES.platformAuthSecret]: "kept-auth-secret" }; + const { store, changed } = convergeStore(installation(), stored, { readFile: files(), env: {}, redactor }); + expect(store[APP_SECRET_NAMES.platformAuthSecret]).toBe("kept-auth-secret"); + expect(changed).not.toContain(APP_SECRET_NAMES.platformAuthSecret); + expect(redactor.redact("kept-auth-secret")).toBe("[REDACTED]"); + }); + + test("convergeTls re-issues a leaf close to its expiry and keeps the CA", async () => { + const config = installation(); + const issued = await convergeTls(config, {}, new Date("2026-10-20T00:00:00Z"), () => {}); + const lines: string[] = []; + const later = await convergeTls(config, issued.data, new Date("2029-01-05T00:00:00Z"), (line) => lines.push(line)); + expect(later.changed).toBe(true); + expect(later.data["ca.crt"]).toBe(issued.data["ca.crt"] as string); + expect(later.data["postgres.crt"]).not.toBe(issued.data["postgres.crt"] as string); + expect(lines.join("\n")).toContain("days left"); + const kept = await convergeTls(config, issued.data, new Date("2026-10-21T00:00:00Z"), () => {}); + expect(kept.changed).toBe(false); + }); + + test("observedTls reads only the keys that exist", () => { + expect(observedTls({ "ca.crt": "c" }, ["postgres"])).toEqual({ ca: { certPem: "c" }, leaves: { postgres: {} } }); + }); + + test("caBundleOf refuses a configured CA file that is not mounted", async () => { + const ca = await createCa({ commonName: "internal", now: new Date() }); + expect(() => caBundleOf(installation(EXTERNAL_POSTGRES), ca.certPem, () => undefined)).toThrow("does not exist"); + }); + + test("the edge Caddyfile serves the auth site alone behind the ingress", () => { + const caddyfile = keycloakEdgeCaddyfile(installation(), manifestFixture()); + expect(caddyfile).toContain("trusted_proxies static private_ranges"); + expect(caddyfile).toContain("respond 400"); + }); + + test("the state round-trips through its Secret, and the first run records the immutable fields", async () => { + const objects = new MemoryObjects(); + expect(await loadState(objects)).toEqual(emptyState()); + const state = checkInstallation(emptyState(), installation()); + expect(state.installation).toBeDefined(); + await saveState(objects, state); + expect(objects.secret(K8S_NAMES.state)[STATE_KEY]).toEndWith("\n"); + expect(await loadState(objects)).toEqual(state); + }); +}); + +describe("the bootstrap job across upgrades", () => { + const SECRETS_MODE = { tls: { mode: "external", caBundleFile: `${INPUT_DIR}/ca/tls-ca.crt`, external: { trustedProxies: ["private_ranges"] } } }; + + test("asks to restart only the workloads whose inputs changed, and nothing on a run that changes nothing", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + // A first install asks for every reader; the restart job then finds their pods started after the change. + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))?.workloads).toHaveLength(4); + await runRestarts(objects, "1.0.0", () => new Date(), () => {}); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))).toBeUndefined(); + const lines = await bootstrap(objects); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))).toBeUndefined(); + expect(lines.join("\n")).not.toContain("asked to restart"); + const renewed = "ldl1.fedcba9876543210.eyJpZCI6IlJFTkVXRUQifQ.c2lnbmF0dXJl"; + await bootstrap(objects, installation(), files({ [`${INPUT_DIR}/license/license`]: renewed })); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))?.workloads).toEqual([WORKLOADS.platform]); + }); + + test("a renewed PostgreSQL leaf asks to restart the server", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + await runRestarts(objects, "1.0.0", () => new Date(), () => {}); + await bootstrap(objects, installation(), files(), new Date("2029-01-05T00:00:00Z")); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))?.workloads).toEqual([WORKLOADS.postgres]); + }); + + test("the restart job restarts old pods, skips pods started after the change and absent workloads, then clears the request", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects, installation(), files(), new Date("2026-10-20T00:00:00Z")); + objects.workloads.add("deployments/libredb-platform"); + objects.workloads.add("deployments/libredb-studio"); + objects.pods.set("deployments/libredb-studio", [new Date("2026-10-20T00:05:00Z")]); + const lines: string[] = []; + await runRestarts(objects, "1.0.0", () => new Date("2026-10-20T00:10:00Z"), (line) => lines.push(line)); + expect(objects.restarts).toEqual(["deployments/libredb-platform"]); + expect(lines).toContain("deployments/libredb-studio started after its inputs changed"); + expect(lines).toContain("deployments/keycloak does not exist"); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))).toBeUndefined(); + expect(objects.secret(K8S_NAMES.state)[RELEASE_KEY]).toBeDefined(); + const again: string[] = []; + await runRestarts(objects, "1.0.0", () => new Date(), (line) => again.push(line)); + expect(again).toEqual(["no restart asked for", "the installation runs LibreDB 1.0.0"]); + }); + + test("a restart request survives a run that fails after it, and a later run adds to it with the newer time", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + await runRestarts(objects, "1.0.0", () => new Date(), () => {}); + const renewed = (id: string) => `ldl1.fedcba9876543210.${Buffer.from(JSON.stringify({ id })).toString("base64url")}.c2lnbmF0dXJl`; + await bootstrap(objects, installation(), files({ [`${INPUT_DIR}/license/license`]: renewed("A") }), new Date("2026-10-21T00:00:00Z")); + const failing = Object.assign(Object.create(Object.getPrototypeOf(objects)), objects) as MemoryObjects; + failing.restart = async () => { + throw new Error("the API server is down"); + }; + objects.workloads.add("deployments/libredb-platform"); + await expect(runRestarts(failing, "1.0.0", () => new Date(), () => {})).rejects.toThrow("down"); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))?.workloads).toEqual([WORKLOADS.platform]); + await bootstrap(objects, installation(INSTALLER_KEYCLOAK), files({ [`${INPUT_DIR}/license/license`]: renewed("A") }), new Date("2026-10-22T00:00:00Z")).catch(() => {}); + expect(addPendingRestarts(pendingRestartsOf(objects.secret(K8S_NAMES.state)), [WORKLOADS.studio], new Date("2026-10-23T00:00:00Z"))).toEqual({ + since: "2026-10-23T00:00:00.000Z", + workloads: [WORKLOADS.platform, WORKLOADS.studio], + }); + }); + + test("a switch to tls.mode secrets writes no ingress Secret any more, though the store keeps the old leaves", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + for (const name of [K8S_NAMES.ingressTls("platform"), K8S_NAMES.ingressTls("studio"), K8S_NAMES.ingressTls("auth")]) objects.secrets.delete(name); + const ca = await createCa({ commonName: "site", now: new Date() }); + await bootstrap(objects, installation(SECRETS_MODE), files({ [`${INPUT_DIR}/ca/tls-ca.crt`]: ca.certPem })); + expect(objects.secrets.has(K8S_NAMES.ingressTls("platform"))).toBe(false); + expect(objects.secret(K8S_NAMES.tls)["platform.crt"]).toBeDefined(); + }); + + test("Studio's network environment holds the installer's NO_PROXY, and the proxy when one is set", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects, installation({ network: { httpsProxy: "http://proxy.corp:3128" } })); + const env = objects.secret(K8S_NAMES.studioNetworkEnv); + expect(env.HTTPS_PROXY).toBe("http://proxy.corp:3128"); + expect(env.NODE_USE_ENV_PROXY).toBe("1"); + expect(env.NO_PROXY).toContain("libredb-postgres"); + expect(env.NO_PROXY).toBe(objects.secret(K8S_NAMES.platformEnv).NO_PROXY as string); + }); + + test("the release is recorded once the restart job ran, kept by later state saves, and an older one is refused", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + // A failed upgrade, whose restart job never ran, records nothing, so its --atomic rollback is not refused. + expect(objects.secret(K8S_NAMES.state)[RELEASE_KEY]).toBeUndefined(); + await runRestarts(objects, manifestFixture().version, () => new Date(), () => {}); + expect(objects.secret(K8S_NAMES.state)[RELEASE_KEY]).toBe(manifestFixture().version); + await saveState(objects, emptyState()); + expect(objects.secret(K8S_NAMES.state)[RELEASE_KEY]).toBe(manifestFixture().version); + await expect(checkRelease(objects, "0.9.0")).rejects.toThrow("a downgrade is not supported"); + await expect(checkRelease(objects, "0.9.0")).rejects.toMatchObject({ name: "RefusedError", exitCode: 6 }); + await checkRelease(objects, "9.0.0"); + }); + + test("sameData compares keys and values", () => { + expect(sameData({ a: "1" }, { a: "1" })).toBe(true); + expect(sameData({ a: "1" }, { a: "2" })).toBe(false); + expect(sameData({ a: "1" }, { a: "1", b: "2" })).toBe(false); + }); +}); + +describe("the bootstrap job's seal key rotation", () => { + type StoredKey = { kid: string; publicKey: string; state: string }; + const storedKeys = (objects: MemoryObjects) => JSON.parse(objects.secret(K8S_NAMES.secretStore)[STORE_KEYS.sealKeys] as string) as StoredKey[]; + const keyringSize = (objects: MemoryObjects) => + (JSON.parse(objects.secret(STUDIO_SECRET)[STUDIO_SECRET_KEYS.governancePrivateKeys] as string) as { keys: string[] }).keys.length; + const sealing = (sealKeys: Record) => installation({ bridge: { sealKeys } }); + const uses = (rows: unknown[]) => files({ [SEAL_ENVELOPES_FILE]: `libredb-result envelope-uses ${JSON.stringify(rows)}\n` }); + + test("walks the runbook: issue a key, switch Platform to it, retire the old one, each step once", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const [old] = storedKeys(objects) as [StoredKey]; + + const issued = await bootstrap(objects, sealing({ issueNew: true })); + const [, fresh] = storedKeys(objects) as [StoredKey, StoredKey]; + expect(issued).toContain(`seal keys: issued seal key ${fresh.kid} into the Studio keyring`); + expect(keyringSize(objects)).toBe(2); + // Platform keeps sealing to the oldest live key until activeKid names the new one. + expect(objects.secret(K8S_NAMES.platformEnv).BRIDGE_SEAL_PUBLIC_KEY).toBe(old.publicKey); + // The same values again issue no second key. + await bootstrap(objects, sealing({ issueNew: true })); + expect(storedKeys(objects)).toHaveLength(2); + + await bootstrap(objects, sealing({ issueNew: false, activeKid: fresh.kid })); + expect(objects.secret(K8S_NAMES.platformEnv).BRIDGE_SEAL_PUBLIC_KEY).toBe(fresh.publicKey); + expect(pendingRestartsOf(objects.secret(K8S_NAMES.state))?.workloads).toContainEqual(WORKLOADS.platform); + + const retired = await bootstrap(objects, sealing({ issueNew: false, activeKid: fresh.kid, retire: [old.kid] }), uses([])); + expect(retired).toContain(`seal keys: retired seal key ${old.kid}: out of the Studio keyring, kept in the secret store`); + expect(storedKeys(objects).map((key) => [key.kid, key.state])).toEqual([ + [old.kid, "retired"], + [fresh.kid, "live"], + ]); + expect(keyringSize(objects)).toBe(1); + // Retiring an already retired kid needs neither the envelopes nor a change. + const again = await bootstrap(objects, sealing({ issueNew: false, activeKid: fresh.kid, retire: [old.kid] }), files()); + expect(again.join("\n")).not.toContain("seal keys:"); + }); + + test("refuses with exit 6 a retirement while a connection still uses the kid, and changes nothing", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + await bootstrap(objects, sealing({ issueNew: true })); + const [old, fresh] = storedKeys(objects) as [StoredKey, StoredKey]; + const before = objects.secret(K8S_NAMES.secretStore); + const config = sealing({ activeKid: fresh.kid, retire: [old.kid] }); + const run = bootstrap(objects, config, uses([{ kid: old.kid, connectionId: "c1", name: "orders" }])); + await expect(run).rejects.toMatchObject({ exitCode: 6 }); + await expect(bootstrap(objects, config, uses([{ kid: old.kid, connectionId: "c1", name: "orders" }]))).rejects.toThrow( + `kid ${old.kid} still seals credentials of 1 connection in bridge.connections_v1: orders (c1)`, + ); + expect(objects.secret(K8S_NAMES.secretStore)).toEqual(before); + }); + + test("refuses to retire the active kid, and needs the init container's output for a live kid", async () => { + const objects = new MemoryObjects(); + await bootstrap(objects); + const [only] = storedKeys(objects) as [StoredKey]; + await expect(bootstrap(objects, sealing({ retire: [only.kid] }), uses([]))).rejects.toMatchObject({ exitCode: 6 }); + await bootstrap(objects, sealing({ issueNew: true })); + const [, fresh] = storedKeys(objects) as [StoredKey, StoredKey]; + await expect(bootstrap(objects, sealing({ activeKid: fresh.kid, retire: [only.kid] }))).rejects.toThrow( + `retiring seal key ${only.kid} needs the connections each still seals`, + ); + }); + + test("rotateSealKeys leaves the keys alone without bridge.sealKeys", () => { + const keys = sealKeysOf(undefined).keys; + expect(rotateSealKeys(keys, installation(), files())).toEqual({ keys, done: [] }); + }); +}); diff --git a/installer/test/unit/kubernetes/bundle.test.ts b/installer/test/unit/kubernetes/bundle.test.ts new file mode 100644 index 0000000..d939c12 --- /dev/null +++ b/installer/test/unit/kubernetes/bundle.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "bun:test"; +import { buildHelmJobs, committedBundle } from "../../../scripts/build-helm-jobs"; + +describe("the Helm chart's job bundle", () => { + test("is the bundle of the installer's current code: run `bun run build:helm-jobs` after changing what the jobs import", () => { + expect(committedBundle() === buildHelmJobs()).toBe(true); + }, 60_000); +}); diff --git a/installer/test/unit/kubernetes/chart.test.ts b/installer/test/unit/kubernetes/chart.test.ts new file mode 100644 index 0000000..46694fc --- /dev/null +++ b/installer/test/unit/kubernetes/chart.test.ts @@ -0,0 +1,451 @@ +// The Helm chart charts/libredb-platform against the installer it drives, through the real `helm template` output: +// every render guard, the installation document the jobs validate with the installer's schema, the image and Studio +// chart pins of the release manifest, the environments of Studio and Keycloak against the installer's renderers, the +// bootstrap Role against every object the bootstrap job writes, and the pod security of every workload. +import { beforeAll, describe, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { parse, parseAllDocuments } from "yaml"; +import { parseInstallation } from "../../../src/config/schema"; +import { runBootstrap } from "../../../src/kubernetes/bootstrap"; +import { computeDesired } from "../../../src/model/desired"; +import { renderStudioEnv } from "../../../src/model/studio-env"; +import { parseReleaseManifest } from "../../../src/release/manifest"; +import { composeTrust, renderCompose } from "../../../src/render/compose"; +import manifestDocument from "../../../release/release.config.json" with { type: "json" }; +import { files, MemoryObjects } from "./fixture"; +import { Redactor } from "../../../src/log/redact"; +import { INPUT_DIR, WORKLOADS } from "../../../src/kubernetes/names"; +import { deleteCanaryScript, envelopeUsesScript } from "../../../src/secrets/seal-rotation"; +import { SEAL_ENVELOPES_FILE } from "../../../src/kubernetes/bootstrap"; +import { createCa } from "../../../src/tls/ca"; + +const CHART = join(import.meta.dir, "..", "..", "..", "..", "charts", "libredb-platform"); +const LICENSE = "ldl1.0123456789abcdef.eyJpZCI6IlRFU1QifQ.c2lnbmF0dXJl"; +const manifest = parseReleaseManifest(manifestDocument); + +type Doc = { kind: string; metadata: { name: string; annotations?: Record }; spec?: any; data?: Record; rules?: any[] }; + +const BASE = [ + "hosts.platform=platform.libredb.test", + "hosts.studio=studio.libredb.test", + "hosts.auth=auth.libredb.test", + "platform.organizationName=Acme", + `platform.license.value=${LICENSE}`, + "studio.config.oidcIssuer=https://auth.libredb.test/realms/libredb", + "studio.ingress.hosts[0].host=studio.libredb.test", + "studio.ingress.hosts[0].paths[0].path=/", + "studio.ingress.hosts[0].paths[0].pathType=Prefix", + "studio.ingress.tls[0].secretName=libredb-tls-studio", + "studio.ingress.tls[0].hosts[0]=studio.libredb.test", +]; + +function helm(sets: readonly string[], args: readonly string[] = []): string { + return execFileSync("helm", ["template", "libredb", CHART, "-n", "libredb", ...sets.flatMap((set) => ["--set", set]), ...args], { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); +} + +function render(sets: readonly string[] = [], args: readonly string[] = []): Doc[] { + return parseAllDocuments(helm([...BASE, ...sets], args)) + .map((doc) => doc.toJS() as Doc | null) + .filter((doc): doc is Doc => doc !== null); +} + +function failure(sets: readonly string[]): string { + try { + helm([...BASE, ...sets]); + } catch (error) { + return String((error as { stderr?: string }).stderr); + } + throw new Error("the render succeeded"); +} + +function find(docs: Doc[], kind: string, name: string): Doc { + const doc = docs.find((candidate) => candidate.kind === kind && candidate.metadata.name === name); + if (doc === undefined) throw new Error(`no ${kind} ${name}`); + return doc; +} + +function installationOf(docs: Doc[]) { + return JSON.parse(find(docs, "ConfigMap", "libredb-bootstrap-chart").data?.["installation.json"] as string); +} + +beforeAll(() => { + // The Studio chart dependency: the one `bun run embed` downloaded and checked against the manifest's sha256 when it + // exists, as in CI, or else Helm's build from Chart.lock. + const archive = `libredb-studio-${manifest.studioChart.version}.tgz`; + if (existsSync(join(CHART, "charts", archive))) return; + const embedded = join(import.meta.dir, "..", "..", "..", "generated", archive); + if (existsSync(embedded)) { + mkdirSync(join(CHART, "charts"), { recursive: true }); + copyFileSync(embedded, join(CHART, "charts", archive)); + } else { + execFileSync("helm", ["dependency", "build", CHART], { stdio: "ignore" }); + } +}); + +describe("the chart's pins", () => { + test("its images are the release manifest's, by digest", () => { + const values = parse(readFileSync(join(CHART, "values.yaml"), "utf8")); + const pinned = (entry: { repository: string; version: string; digest: string }) => ({ repository: entry.repository, tag: `${entry.version}@${entry.digest}` }); + const pick = (image: { repository: string; tag: string }) => ({ repository: image.repository, tag: image.tag }); + expect(pick(values.platform.image)).toEqual(pinned(manifest.images.platform)); + expect(pick(values.studio.image)).toEqual(pinned(manifest.images.studio)); + expect(pick(values.keycloak.managed.image)).toEqual(pinned(manifest.images.keycloak)); + expect(pick(values.postgresql.managed.image)).toEqual(pinned(manifest.images.postgres)); + expect(pick(values.keycloak.managed.edge.image)).toEqual(pinned(manifest.images.edge)); + }); + + test("its appVersion is the release, and its Studio dependency the manifest's chart with its sha256", () => { + const chart = parse(readFileSync(join(CHART, "Chart.yaml"), "utf8")); + expect(chart.appVersion).toBe(manifest.version); + const studio = chart.dependencies.find((dependency: { name: string }) => dependency.name === "libredb-studio"); + expect({ version: studio.version, repository: studio.repository }).toEqual({ + version: manifest.studioChart.version, + repository: manifest.studioChart.repository, + }); + const archive = readFileSync(join(CHART, "charts", `libredb-studio-${manifest.studioChart.version}.tgz`)); + expect(new Bun.CryptoHasher("sha256").update(archive).digest("hex")).toBe(manifest.studioChart.sha256); + }); +}); + +describe("the chart's render guards", () => { + test.each([ + [["hosts.auth="], "hosts.auth is required"], + [["platform.organizationName="], "platform.organizationName is required"], + [["platform.license.value="], "platform.license is required"], + [["platform.license.value=not-a-token"], "must hold the license token alone"], + [["tls.mode=letsencrypt"], "tls.mode must be generated or secrets"], + [["tls.mode=secrets"], "tls.mode secrets needs tls.secrets.platform"], + [["tls.mode=secrets", "tls.secrets.platform=a", "tls.secrets.studio=b", "tls.secrets.auth=c"], "tls.mode secrets needs tls.caBundle"], + [["tls.mode=secrets", "tls.secrets.platform=a", "tls.secrets.studio=b"], "needs tls.secrets.auth"], + [["postgresql.mode=cloud"], "postgresql.mode must be managed or external"], + [["postgresql.mode=external"], "postgresql.mode external needs postgresql.external.host"], + [["keycloak.mode=cloud"], "keycloak.mode must be managed or external"], + [["keycloak.mode=external"], "keycloak.mode external needs keycloak.external.baseUrl"], + [["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.provisioning=auto"], "must be installer or manual"], + [["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test"], "needs keycloak.external.adminSecret"], + [["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.adminSecret=kc"], "needs exactly one of keycloak.external.adminClientId and keycloak.external.adminUser"], + [["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.adminSecret=kc", "keycloak.external.adminUser=a", "keycloak.external.adminClientId=c"], "needs exactly one of"], + [["network.httpsProxy=http://user:secret@proxy.corp:3128"], "network.httpsProxy must not hold credentials"], + [["network.httpsProxy=HTTP://user:secret@proxy.corp:3128"], "network.httpsProxy must not hold credentials"], + [["studio.fullnameOverride=studio"], "studio.fullnameOverride must stay libredb-studio"], + [["studio.ingress.tls[0].secretName=other"], "studio.ingress.tls must hold one entry with secretName libredb-tls-studio and hosts [studio.libredb.test]"], + [["studio.ingress.tls[0].hosts[0]=other.libredb.test"], "studio.ingress.tls must hold one entry"], + [["tls.mode=secrets", "tls.secrets.platform=a", "tls.secrets.studio=b", "tls.secrets.auth=c", "tls.caBundle=PEM"], "studio.ingress.tls must hold one entry with secretName b"], + [["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.provisioning=manual"], "needs keycloak.external.clientSecretsSecret"], + [["studio.config.oidcIssuer=https://other/realms/libredb"], "studio.config.oidcIssuer must be https://auth.libredb.test/realms/libredb"], + [["studio.secrets.existingSecret=mine"], "studio.secrets.existingSecret must stay libredb-studio-env"], + [["studio.ingress.hosts[0].host=other.libredb.test"], "studio.ingress.hosts must hold exactly one host"], + ])("%j fails naming the value", (sets, message) => { + expect(failure(sets as string[])).toContain(message as string); + }); +}); + +describe("the installation document", () => { + test.each([ + ["managed PostgreSQL and Keycloak", []], + ["an external PostgreSQL with its CA", ["postgresql.mode=external", "postgresql.external.host=pg.corp.example", "postgresql.external.caBundle=PEM"]], + [ + "the site's Keycloak, provisioning manual", + ["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.provisioning=manual", "keycloak.external.clientSecretsSecret=kc"], + ], + [ + "the site's Keycloak, provisioning installer with an admin user", + ["keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.adminSecret=kc", "keycloak.external.adminUser=admin", "keycloak.external.caBundle=PEM"], + ], + [ + "TLS Secrets of the site", + ["tls.mode=secrets", "tls.secrets.platform=a", "tls.secrets.studio=b", "tls.secrets.auth=c", "tls.caBundle=PEM", "studio.ingress.tls[0].secretName=b", "platform.queryLogRetentionDays=30", "network.httpsProxy=http://proxy.corp:3128"], + ], + ])("with %s passes the installer's schema", (_name, sets) => { + const document = installationOf(render(sets as string[])); + expect(() => parseInstallation(document)).not.toThrow(); + expect(document.mode).toBe("kubernetes"); + expect(JSON.stringify(document)).not.toContain(LICENSE); + }); + + test("the first accounts follow keycloak.firstAdmin.enabled and keycloak.userManager.enabled when set", () => { + const document = installationOf(render(["keycloak.firstAdmin.enabled=false", "keycloak.userManager.enabled=true"])); + expect(parseInstallation(document).keycloak.firstAdmin.enabled).toBe(false); + }); +}); + +describe("the seal key and AUTH_SECRET rotations", () => { + const OLD = "0123456789abcdef"; + const NEW = "fedcba9876543210"; + const podOf = (doc: Doc) => doc.spec.template.spec; + + test("platform.sealKeys becomes bridge.sealKeys, and by default no job reaches the database for it", () => { + const defaults = render(); + expect(parseInstallation(installationOf(defaults)).bridge.sealKeys).toEqual({ issueNew: false, retire: [] }); + expect(podOf(find(defaults, "Job", "libredb-bootstrap")).initContainers).toBeUndefined(); + expect(podOf(find(defaults, "Job", "libredb-restart")).initContainers).toBeUndefined(); + const issuing = parseInstallation(installationOf(render(["platform.sealKeys.issueNew=true", `platform.sealKeys.activeKid=${NEW}`]))); + expect(issuing.bridge.sealKeys).toEqual({ issueNew: true, activeKid: NEW, retire: [] }); + }); + + test("a retirement runs the installer's own SQL: the envelope query before the bootstrap, the canary delete after the release", () => { + const docs = render([`platform.sealKeys.activeKid=${NEW}`, `platform.sealKeys.retire[0]=${OLD}`, `platform.sealKeys.retire[1]=${NEW.replace("f", "e")}`]); + const kids = [OLD, NEW.replace("f", "e")]; + expect(parseInstallation(installationOf(docs)).bridge.sealKeys.retire).toEqual(kids); + expect(find(docs, "ConfigMap", "libredb-bootstrap-chart").data?.["seal-envelopes.sql"]).toBe(envelopeUsesScript(kids)); + expect(find(docs, "ConfigMap", "libredb-chart").data?.["seal-canary.sql"]).toBe(kids.map(deleteCanaryScript).join("")); + const bootstrap = podOf(find(docs, "Job", "libredb-bootstrap")); + expect(bootstrap.initContainers.map((container: { name: string }) => container.name)).toEqual(["seal-envelopes"]); + expect(bootstrap.initContainers[0].command[2]).toContain(`-f /etc/libredb/chart/seal-envelopes.sql > ${SEAL_ENVELOPES_FILE}`); + const restart = podOf(find(docs, "Job", "libredb-restart")); + expect(restart.initContainers[0].command[2]).toContain("-f /etc/libredb/chart/seal-canary.sql"); + for (const pod of [bootstrap, restart]) { + const init = pod.initContainers[0]; + expect(init.env).toEqual([{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: "libredb-platform-migrate-env", key: "DATABASE_URL" } } }]); + // libpq gets the password in PGPASSWORD, never on psql's command line. + expect(init.command[2]).toContain('export PGPASSWORD="${rest%%@*}"'); + expect(pod.volumes.find((volume: { name: string }) => volume.name === "installation-ca").configMap.name).toBe("libredb-ca"); + } + }); + + test("a kid that is no seal key id fails the render, naming the value", () => { + expect(failure(["platform.sealKeys.retire[0]=x'; DROP TABLE y;--"])).toContain("platform.sealKeys.retire[0]"); + expect(failure(["platform.sealKeys.activeKid=ABCDEF0123456789"])).toContain("platform.sealKeys.activeKid"); + }); + + test("platform.authSecretPrevious mounts its Secret where platform.authSecretPreviousFile names it", () => { + const docs = render(["platform.authSecretPrevious.existingSecret=auth-previous"]); + const file = parseInstallation(installationOf(docs)).platform.authSecretPreviousFile; + expect(file).toBe(`${INPUT_DIR}/auth-secret-previous/auth-secret-previous`); + const pod = podOf(find(docs, "Job", "libredb-bootstrap")); + const mount = pod.containers[0].volumeMounts.find((volume: { name: string }) => volume.name === "auth-secret-previous"); + expect(`${mount.mountPath}/auth-secret-previous`).toBe(file as string); + expect(pod.volumes.find((volume: { name: string }) => volume.name === "auth-secret-previous").secret).toEqual({ + secretName: "auth-previous", + items: [{ key: "auth-secret-previous", path: "auth-secret-previous" }], + }); + expect(installationOf(render()).platform.authSecretPreviousFile).toBeUndefined(); + }); +}); + +describe("the workloads", () => { + const docs = () => render(); + + test("serve gets only Platform's env Secret, and only the migrate job gets the owner's URL", () => { + const all = docs(); + const platform = find(all, "Deployment", "libredb-platform").spec.template.spec.containers[0]; + expect(platform.args).toEqual(["serve"]); + expect(platform.envFrom).toEqual([{ secretRef: { name: "libredb-platform-env" } }]); + expect(platform.env).toBeUndefined(); + const migrate = all.find((doc) => doc.kind === "Job" && doc.metadata.name.startsWith("libredb-platform-migrate-")); + expect(migrate?.spec.template.spec.containers[0].envFrom).toEqual([{ secretRef: { name: "libredb-platform-migrate-env" } }]); + const mentions = all.filter((doc) => JSON.stringify(doc).includes("libredb-platform-migrate-env")).map((doc) => `${doc.kind}/${doc.metadata.name}`); + expect(mentions.every((name) => name.startsWith("Job/libredb-platform-migrate-") || name === "Role/libredb-bootstrap")).toBe(true); + }); + + test("every pod runs as non-root with the RuntimeDefault seccomp profile, and only the three jobs mount a token", () => { + for (const doc of docs()) { + const pod = doc.kind === "Job" || doc.kind === "Deployment" || doc.kind === "StatefulSet" ? doc.spec.template.spec : undefined; + if (pod === undefined) continue; + const name = `${doc.kind}/${doc.metadata.name}`; + expect(pod.securityContext?.runAsNonRoot ?? true, name).toBe(true); + expect(pod.securityContext?.seccompProfile?.type, name).toBe("RuntimeDefault"); + const tokenJob = ["libredb-bootstrap", "libredb-restart"].includes(doc.metadata.name) || doc.metadata.name.startsWith("libredb-keycloak-configure-"); + if (!doc.metadata.name.startsWith("libredb-studio")) expect(pod.automountServiceAccountToken, name).toBe(tokenJob); + for (const container of pod.containers) { + expect(container.securityContext?.allowPrivilegeEscalation, `${name} ${container.name}`).toBe(false); + expect(container.securityContext?.capabilities?.drop, `${name} ${container.name}`).toEqual(["ALL"]); + } + } + }); + + test("on OpenShift every pod, Studio's included, leaves its uids to the SCC; elsewhere each keeps its image's", () => { + const uids = (all: Doc[]) => + all + .filter((doc) => doc.kind === "Job" || doc.kind === "Deployment" || doc.kind === "StatefulSet") + .map((doc) => { + const security = doc.spec.template.spec.securityContext ?? {}; + return [`${doc.kind}/${doc.metadata.name}`, security.runAsUser, security.runAsGroup, security.fsGroup]; + }); + for (const [name, user, group, fs] of uids(docs())) { + expect([user, group, fs].every((id) => typeof id === "number" && id > 0), String(name)).toBe(true); + } + for (const all of [render([], ["--api-versions", "security.openshift.io/v1"]), render(["global.compatibility.openshift.adaptSecurityContext=force"])]) { + for (const [name, ...ids] of uids(all)) expect(ids, String(name)).toEqual([undefined, undefined, undefined]); + } + const disabled = render(["global.compatibility.openshift.adaptSecurityContext=disabled"], ["--api-versions", "security.openshift.io/v1"]); + expect(find(disabled, "StatefulSet", "libredb-postgres").spec.template.spec.securityContext.runAsUser).toBe(999); + }); + + test("every pod of the amd64-only Platform image, its jobs included, runs on amd64 nodes and follows Platform's scheduling", () => { + const platformPods = (all: Doc[]) => + all.filter( + (doc) => + (doc.kind === "Job" || doc.kind === "Deployment") && + doc.spec.template.spec.containers.some((container: { image: string }) => container.image.includes("/libredb-platform:")), + ); + const defaults = platformPods(docs()); + expect(defaults.map((doc) => doc.metadata.name.replace(/-[0-9a-f]{8}$/, "")).sort()).toEqual([ + "libredb-bootstrap", + "libredb-keycloak-configure", + "libredb-platform", + "libredb-platform-migrate", + "libredb-restart", + ]); + for (const doc of defaults) { + expect(doc.spec.template.spec.nodeSelector, doc.metadata.name).toEqual({ "kubernetes.io/arch": "amd64" }); + expect(doc.spec.template.spec.tolerations, doc.metadata.name).toBeUndefined(); + } + const placed = render(["platform.nodeSelector.pool=apps", "platform.tolerations[0].key=dedicated", "platform.tolerations[0].operator=Exists"]); + for (const doc of platformPods(placed)) { + expect(doc.spec.template.spec.nodeSelector, doc.metadata.name).toEqual({ "kubernetes.io/arch": "amd64", pool: "apps" }); + expect(doc.spec.template.spec.tolerations, doc.metadata.name).toEqual([{ key: "dedicated", operator: "Exists" }]); + } + const arm = render(["platform.nodeSelector.kubernetes\\.io/arch=arm64"]); + expect(find(arm, "Deployment", "libredb-platform").spec.template.spec.nodeSelector).toEqual({ "kubernetes.io/arch": "arm64" }); + }); + + test("Studio's environment under the chart's defaults equals the installer's renderStudioEnv", () => { + const all = docs(); + const configMap = find(all, "ConfigMap", "libredb-studio-config").data ?? {}; + const container = find(all, "Deployment", "libredb-studio").spec.template.spec.containers[0]; + const env = new Map(Object.entries(configMap)); + for (const entry of container.env as { name: string; value?: string }[]) if (entry.value !== undefined) env.set(entry.name, entry.value); + const config = parseInstallation(installationOf(all)); + const expected = renderStudioEnv(computeDesired(config, manifest), { + postgres: { host: "libredb-postgres", port: 5432 }, + secrets: { oidcClientSecret: "s", jwtSecret: "j", storageEncryptionKey: "k", databasePassword: "p" }, + }); + const secretKeys = new Set(["OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "JWT_SECRET", "STORAGE_POSTGRES_URL", "GOVERNANCE_POSTGRES_URL", "STORAGE_ENCRYPTION_KEY"]); + for (const [key, value] of Object.entries(expected)) { + if (value === undefined || secretKeys.has(key) || key === "NO_PROXY" || key === "no_proxy") continue; + expect(`${key}=${env.get(key)}`).toBe(`${key}=${value}`); + } + const fromSecret = (container.env as { name: string; valueFrom?: { secretKeyRef?: { name: string } } }[]).filter((entry) => entry.valueFrom?.secretKeyRef?.name === "libredb-studio-env").map((entry) => entry.name); + expect(fromSecret).toEqual(expect.arrayContaining(["JWT_SECRET", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "STORAGE_POSTGRES_URL", "GOVERNANCE_POSTGRES_URL", "STORAGE_ENCRYPTION_KEY"])); + }); + + test("the managed Keycloak's environment equals the installer's compose service", () => { + const all = docs(); + const container = find(all, "Deployment", "keycloak").spec.template.spec.containers.find((candidate: { name: string }) => candidate.name === "keycloak"); + const env = Object.fromEntries((container.env as { name: string; value: string }[]).map((entry) => [entry.name, entry.value])); + const compose = parse(renderCompose(computeDesired(parseInstallation(installationOf(all)), manifest), composeTrust(undefined, []))); + expect(env).toEqual(compose.services.keycloak.environment); + expect(container.envFrom).toEqual([{ secretRef: { name: "libredb-keycloak-env" } }]); + }); + + test("the auth ingress reaches only the edge, and the Admin API stays in-cluster", () => { + const ingress = find(docs(), "Ingress", "keycloak"); + expect(ingress.spec.rules[0].http.paths[0].backend.service.name).toBe("keycloak-edge"); + }); + + test("a job's name follows the revision and what it runs: the same values keep it, a new installation document renames it", () => { + const names = (sets: string[]) => render(sets).filter((doc) => doc.kind === "Job").map((doc) => doc.metadata.name).sort(); + expect(names([])).toEqual(names([])); + const changed = names(["platform.organizationName=Other"]); + expect(changed.filter((name) => names([]).includes(name))).toEqual(["libredb-bootstrap", "libredb-restart"]); + }); + + test("the uploads volume outlives helm uninstall, as the database's volume claims do by Kubernetes' rule", () => { + const all = docs(); + expect(find(all, "PersistentVolumeClaim", "libredb-platform-uploads").metadata.annotations?.["helm.sh/resource-policy"]).toBe("keep"); + expect(find(all, "StatefulSet", "libredb-postgres").spec.volumeClaimTemplates[0].metadata.annotations).toBeUndefined(); + const off = render(["platform.persistence.enabled=false"]); + expect(off.some((doc) => doc.kind === "PersistentVolumeClaim")).toBe(false); + }); + + test("network policies guard PostgreSQL and Keycloak's Admin API by default, and render nothing when disabled", () => { + expect(docs().filter((doc) => doc.kind === "NetworkPolicy").map((doc) => doc.metadata.name).sort()).toEqual(["keycloak", "libredb-postgres"]); + expect(render(["networkPolicy.enabled=false"]).some((doc) => doc.kind === "NetworkPolicy")).toBe(false); + }); + + test("the jobs after the hook are Argo CD sync hooks, so a sync runs them again as an upgrade does", () => { + for (const job of docs().filter((doc) => doc.kind === "Job" && doc.metadata.name !== "libredb-bootstrap" && !doc.metadata.annotations?.["helm.sh/hook"])) { + expect(job.metadata.annotations?.["argocd.argoproj.io/hook"], job.metadata.name).toBe("Sync"); + expect(job.metadata.annotations?.["argocd.argoproj.io/hook-delete-policy"], job.metadata.name).toBe("BeforeHookCreation"); + } + }); + + test("the bootstrap and keycloak jobs fail at once on a refusal a retry cannot change", () => { + for (const job of docs().filter((doc) => doc.kind === "Job" && (doc.metadata.name === "libredb-bootstrap" || doc.metadata.name.startsWith("libredb-keycloak-configure-")))) { + const rule = job.spec.podFailurePolicy.rules[0]; + expect(rule.action, job.metadata.name).toBe("FailJob"); + expect(rule.onExitCodes.values).toEqual([2, 3, 6]); + expect(rule.onExitCodes.containerName).toBe(job.spec.template.spec.containers[0].name); + } + }); + + test("the bootstrap also runs before a rollback, where it refuses an older release, and the restart job after each change", () => { + const all = docs(); + expect(find(all, "Job", "libredb-bootstrap").metadata.annotations?.["helm.sh/hook"]).toBe("pre-install,pre-upgrade,pre-rollback"); + const restart = find(all, "Job", "libredb-restart"); + expect(restart.metadata.annotations?.["helm.sh/hook"]).toBe("post-install,post-upgrade,post-rollback"); + expect(restart.spec.template.spec.serviceAccountName).toBe("libredb-restart"); + }); + + test("the restart Role may patch exactly the workloads the bootstrap job asks to restart", () => { + const role = find(docs(), "Role", "libredb-restart"); + const names = (resource: string) => role.rules?.find((rule) => rule.resources[0] === resource)?.resourceNames ?? []; + const expected = Object.values(WORKLOADS); + expect([...names("deployments")].sort()).toEqual(expected.filter((w) => w.kind === "deployments").map((w) => w.name).sort()); + expect(names("statefulsets")).toEqual(expected.filter((w) => w.kind === "statefulsets").map((w) => w.name)); + const studio = find(docs(), "Deployment", WORKLOADS.studio.name); + expect(studio.metadata.name).toBe(WORKLOADS.studio.name); + }); + + test("the objects the jobs write carry the installation's label and no Helm or Argo CD tracking label", () => { + const settings = JSON.parse(find(docs(), "ConfigMap", "libredb-bootstrap-chart").data?.["chart.json"] as string); + expect(settings.labels["install.libredb.org/installation"]).toBe("libredb"); + expect(settings.labels["app.kubernetes.io/instance"]).toBeUndefined(); + }); + + test("the migrate job's wait passes the password in PGPASSWORD, never on a command line", () => { + const migrate = docs().find((doc) => doc.kind === "Job" && doc.metadata.name.startsWith("libredb-platform-migrate-")); + const script = migrate?.spec.template.spec.initContainers[0].command.join("\n") as string; + expect(script).toContain('export PGPASSWORD="${rest%%@*}"'); + expect(script).not.toContain('psql "$url"'); + }); +}); + +describe("the bootstrap Role", () => { + async function written(sets: string[], inputs: Record = {}) { + const all = render(sets); + const config = parseInstallation(installationOf(all)); + const objects = new MemoryObjects(); + await runBootstrap( + { config, manifest, studioSecretName: "libredb-studio-env", jobsBundle: new Uint8Array([1]) }, + { objects, readFile: files(inputs), env: {}, now: () => new Date(), log: () => {}, redactor: new Redactor() }, + ); + const role = find(all, "Role", "libredb-bootstrap"); + const allowed = (resource: string) => new Set(role.rules?.find((rule) => rule.resources.length === 1 && rule.resources[0] === resource)?.resourceNames ?? []); + return { objects, secrets: allowed("secrets"), configMaps: allowed("configmaps") }; + } + + test.each([ + ["managed PostgreSQL and Keycloak", [] as string[]], + ["the site's TLS Secrets", ["tls.mode=secrets", "tls.secrets.platform=a", "tls.secrets.studio=b", "tls.secrets.auth=c", "tls.caBundle=PEM", "studio.ingress.tls[0].secretName=b"]], + ])("with %s may replace exactly the objects the job writes", async (_name, sets) => { + const ca = (await createCa({ commonName: "site", now: new Date() })).certPem; + const { objects, secrets, configMaps } = await written( + sets.map((set) => (set === "tls.caBundle=PEM" ? `tls.caBundle=${ca.replaceAll(",", "\\,")}` : set)), + { [`${INPUT_DIR}/ca/tls-ca.crt`]: ca }, + ); + expect([...objects.secrets.keys()].sort()).toEqual([...secrets].sort()); + expect([...objects.configMaps.keys()].sort()).toEqual([...configMaps].sort()); + }); + + test("with an external PostgreSQL and the site's Keycloak may replace exactly the objects the job writes", async () => { + const ca = (await createCa({ commonName: "pg", now: new Date() })).certPem; + const { objects, secrets, configMaps } = await written( + ["postgresql.mode=external", "postgresql.external.host=pg.corp.example", "postgresql.external.caBundle=PEM", "keycloak.mode=external", "keycloak.external.baseUrl=https://auth.libredb.test", "keycloak.external.provisioning=manual", "keycloak.external.clientSecretsSecret=kc"], + { + [`${INPUT_DIR}/ca/postgres-ca.crt`]: ca, + [`${INPUT_DIR}/keycloak-clients/studio-client-secret`]: "s", + [`${INPUT_DIR}/keycloak-clients/platform-client-secret`]: "p", + }, + ); + expect([...objects.secrets.keys()].sort()).toEqual([...secrets].sort()); + expect([...objects.configMaps.keys()].sort()).toEqual([...configMaps].sort()); + }); +}); diff --git a/installer/test/unit/kubernetes/fixture.ts b/installer/test/unit/kubernetes/fixture.ts new file mode 100644 index 0000000..dadd81a --- /dev/null +++ b/installer/test/unit/kubernetes/fixture.ts @@ -0,0 +1,127 @@ +// Fixtures of the Helm chart's jobs: installations of mode kubernetes as the chart renders them, an in-memory +// namespace, and the input files the chart mounts. +import { parseInstallation, type LibreDBInstallation } from "../../../src/config/schema"; +import type { KubernetesObjects, ObjectData } from "../../../src/kubernetes/bootstrap"; +import { INPUT_DIR, type Workload } from "../../../src/kubernetes/names"; +import { Redactor } from "../../../src/log/redact"; + +export const LICENSE = "ldl1.0123456789abcdef.eyJpZCI6IlRFU1QifQ.c2lnbmF0dXJl"; + +export const HOSTS = { platform: "platform.libredb.test", studio: "studio.libredb.test", auth: "auth.libredb.test" }; + +/** The installation document of the chart's default values, with optional overrides of whole sections. */ +export function installation(overrides: Record = {}): LibreDBInstallation { + return parseInstallation({ + apiVersion: "install.libredb.org/v1", + kind: "LibreDBInstallation", + metadata: { name: "libredb" }, + mode: "kubernetes", + endpoints: { style: "hostnames", hosts: HOSTS }, + tls: { mode: "generated" }, + postgres: { mode: "managed" }, + keycloak: { mode: "managed", realm: "libredb" }, + platform: { organizationName: "Acme", licenseFile: `${INPUT_DIR}/license/license` }, + kubernetes: { namespace: "libredb" }, + ...overrides, + }); +} + +export const EXTERNAL_POSTGRES = { + postgres: { mode: "external", external: { host: "pg.corp.example", port: 5432, provisioning: "manual", caBundleFile: `${INPUT_DIR}/ca/postgres-ca.crt` } }, +}; + +export const MANUAL_KEYCLOAK = { + keycloak: { + mode: "external", + realm: "corp", + external: { + baseUrl: "https://auth.libredb.test", + provisioning: "manual", + studioClientSecretFile: `${INPUT_DIR}/keycloak-clients/studio-client-secret`, + platformClientSecretFile: `${INPUT_DIR}/keycloak-clients/platform-client-secret`, + }, + }, +}; + +export const INSTALLER_KEYCLOAK = { + keycloak: { + mode: "external", + realm: "corp", + external: { + baseUrl: "https://auth.libredb.test", + adminUrl: "https://kc-admin.corp.example", + provisioning: "installer", + adminRealm: "master", + adminClientId: "corp-admin", + adminClientSecretFile: `${INPUT_DIR}/keycloak-admin/admin-client-secret`, + }, + }, +}; + +/** A namespace's Secrets and ConfigMaps in memory, with every write recorded. */ +export class MemoryObjects implements KubernetesObjects { + readonly secrets = new Map(); + readonly configMaps = new Map(); + readonly binaryData = new Map>>(); + readonly writes: string[] = []; + readonly restarts: string[] = []; + /** The workloads that exist, which a restart finds. */ + readonly workloads = new Set(); + + async getConfigMap(name: string): Promise { + const data = this.configMaps.get(name); + return data === undefined ? undefined : { ...data }; + } + + /** Pod creation times per workload; a workload in `workloads` without an entry has one pod from long ago. */ + readonly pods = new Map(); + + async podsCreated(workload: Workload): Promise { + const key = `${workload.kind}/${workload.name}`; + if (!this.workloads.has(key)) return undefined; + return this.pods.get(key) ?? [new Date("2000-01-01T00:00:00Z")]; + } + + async restart(workload: Workload): Promise<"restarted" | "absent"> { + const key = `${workload.kind}/${workload.name}`; + if (!this.workloads.has(key)) return "absent"; + this.restarts.push(key); + return "restarted"; + } + + async getSecret(name: string): Promise { + const secret = this.secrets.get(name); + return secret === undefined ? undefined : { ...secret.data }; + } + + async putSecret(name: string, data: ObjectData, type: "Opaque" | "kubernetes.io/tls" = "Opaque"): Promise { + this.secrets.set(name, { data: { ...data }, type }); + this.writes.push(`secret/${name}`); + } + + async putConfigMap(name: string, data: ObjectData, binaryData?: Readonly>): Promise { + this.configMaps.set(name, { ...data }); + if (binaryData !== undefined) this.binaryData.set(name, binaryData); + this.writes.push(`configmap/${name}`); + } + + secret(name: string): ObjectData { + const secret = this.secrets.get(name); + if (secret === undefined) throw new Error(`no Secret ${name}`); + return secret.data; + } +} + +/** The input files the chart mounts, by path. */ +export function files(entries: Record = {}): (path: string) => string | undefined { + const all: Record = { [`${INPUT_DIR}/license/license`]: `${LICENSE}\n`, ...entries }; + return (path) => all[path]; +} + +export function io(objects: MemoryObjects, readFile = files(), now = new Date("2026-10-20T00:00:00Z")) { + const lines: string[] = []; + return { + io: { objects, readFile, env: {}, now: () => now, log: (line: string) => lines.push(line), redactor: new Redactor() }, + lines, + }; +} diff --git a/installer/test/unit/kubernetes/job-exit.test.ts b/installer/test/unit/kubernetes/job-exit.test.ts new file mode 100644 index 0000000..1a649c5 --- /dev/null +++ b/installer/test/unit/kubernetes/job-exit.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { ConfigError } from "../../../src/config/schema"; +import { RefusedError } from "../../../src/kubernetes/bootstrap"; +import { UnsafeChangeError } from "../../../src/state/immutable"; + +// The CLI module runs its job on import, so the exit code mapping is checked through a dynamic import guarded by an +// unknown command, which prints the usage and changes nothing. +describe("the jobs' exit code", () => { + test("a refusal keeps the installer's exit code, anything else is a runtime failure", async () => { + const argv = process.argv; + const error = console.error; + process.argv = [argv[0] as string, "job", "--help"]; + console.error = () => {}; + const { jobExitCode } = await import("../../../src/cli/kubernetes-job"); + process.argv = argv; + console.error = error; + process.exitCode = 0; + expect(jobExitCode(new ConfigError(["x: bad"]))).toBe(2); + expect(jobExitCode(new RefusedError("downgrade", 6))).toBe(6); + expect(jobExitCode(new UnsafeChangeError([]))).toBe(6); + expect(jobExitCode(new RefusedError("lock", 5))).toBe(1); + expect(jobExitCode(new TypeError("fetch failed"))).toBe(1); + }); +}); diff --git a/installer/test/unit/kubernetes/keycloak.test.ts b/installer/test/unit/kubernetes/keycloak.test.ts new file mode 100644 index 0000000..56d8656 --- /dev/null +++ b/installer/test/unit/kubernetes/keycloak.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, test } from "bun:test"; +import { CLIENT_SECRET_NAMES } from "../../../src/keycloak/desired"; +import { MANAGED_ATTRIBUTE } from "../../../src/keycloak/representations"; +import { + adminTarget, + ensureManagedRealm, + KEYCLOAK_CONFIG_DIR, + MANAGED_KEYCLOAK_URL, + runKeycloakJob, + waitForKeycloak, + type ConvergeFunctions, + type ConvergeTarget, +} from "../../../src/kubernetes/keycloak"; +import { INPUT_DIR, K8S_NAMES, STORE_KEYS } from "../../../src/kubernetes/names"; +import { Redactor } from "../../../src/log/redact"; +import { STATE_KEY } from "../../../src/kubernetes/bootstrap"; +import type { InstallerState } from "../../../src/state/store"; +import { observedFixture } from "../keycloak/fixture"; +import { manifestFixture } from "../release/fixture"; +import { installation, INSTALLER_KEYCLOAK, MANUAL_KEYCLOAK, MemoryObjects } from "./fixture"; + +const CONFIG: Record = { + [`${KEYCLOAK_CONFIG_DIR}/${STORE_KEYS.keycloakBootstrapSecret}`]: "bootstrap-secret", + [`${KEYCLOAK_CONFIG_DIR}/${CLIENT_SECRET_NAMES.studio}`]: "studio-secret", + [`${KEYCLOAK_CONFIG_DIR}/${CLIENT_SECRET_NAMES.platform}`]: "platform-secret", + [`${KEYCLOAK_CONFIG_DIR}/${STORE_KEYS.firstAdminPassword}`]: "first-admin-temporary", + [`${KEYCLOAK_CONFIG_DIR}/${STORE_KEYS.userManagerPassword}`]: "user-manager-temporary", + [`${INPUT_DIR}/keycloak-admin/admin-client-secret`]: "corp-admin-secret\n", +}; + +const readFile = (path: string) => CONFIG[path]; + +/** A Keycloak that answers the token request and the realm lookup, and records every request. */ +function keycloak(realm: "missing" | "managed" | "foreign") { + const requests: string[] = []; + const fetcher = (async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input); + const method = init?.method ?? "GET"; + requests.push(`${method} ${url}`); + if (url.endsWith("/protocol/openid-connect/token")) return Response.json({ access_token: "token" }); + if (method === "GET" && url.endsWith("/admin/realms/libredb")) { + if (realm === "missing") return new Response("", { status: 404 }); + return Response.json({ attributes: realm === "managed" ? { [MANAGED_ATTRIBUTE]: "true" } : {} }); + } + if (method === "POST" && url.endsWith("/admin/realms")) return new Response("", { status: 201 }); + return new Response("", { status: 500 }); + }) as typeof fetch; + return { fetcher, requests }; +} + +function jobIO(objects: MemoryObjects, fetcher?: typeof fetch) { + const written = new Map(); + const lines: string[] = []; + return { + io: { + objects, + readFile, + writeFile: (path: string, content: string) => written.set(path, content), + env: {}, + tmpDir: "/tmp", + owner: { uid: 1001, gid: 1001 }, + log: (line: string) => lines.push(line), + redactor: new Redactor(), + ...(fetcher === undefined ? {} : { fetch: fetcher }), + }, + written, + lines, + }; +} + +function converge() { + const seen: { target?: ConvergeTarget; applied?: number } = {}; + const functions: ConvergeFunctions = { + observe: async (target) => { + seen.target = target; + return { ...observedFixture("empty-realm"), realm: target.realm }; + }, + apply: async (planned, _target, state: InstallerState) => { + seen.applied = planned.length; + state.ownership.push({ kind: "keycloak-user", name: "libredb-admin", step: "keycloak-configure" }); + }, + }; + return { functions, seen }; +} + +describe("the keycloak-configure job", () => { + test("creates a managed realm, writes the temporary passwords for the step and records the accounts in the state", async () => { + const objects = new MemoryObjects(); + const { fetcher, requests } = keycloak("missing"); + const run = jobIO(objects, fetcher); + const step = converge(); + await runKeycloakJob(installation(), manifestFixture(), run.io, step.functions); + expect(requests).toContain(`POST ${MANAGED_KEYCLOAK_URL}/admin/realms`); + expect(run.written.get("/tmp/first-admin.txt")).toBe("first-admin-temporary\n"); + expect(run.written.get("/tmp/user-manager.txt")).toBe("user-manager-temporary\n"); + expect(step.seen.target?.baseUrl).toBe(MANAGED_KEYCLOAK_URL); + expect(step.seen.target?.credentials).toEqual({ realm: "master", clientId: "libredb-bootstrap", clientSecret: "bootstrap-secret" }); + expect(step.seen.target?.desired.appClients.map((client) => client.representation.secret)).toEqual(["studio-secret", "platform-secret"]); + expect(step.seen.applied).toBe(15); + expect(run.lines).toContain("keycloak: realm libredb created"); + expect(run.lines.at(-1)).toBe("keycloak: applied 15 actions"); + const state = JSON.parse(objects.secret(K8S_NAMES.state)[STATE_KEY] as string) as InstallerState; + expect(state.ownership).toEqual([{ kind: "keycloak-user", name: "libredb-admin", step: "keycloak-configure" }]); + }); + + test("keeps a managed realm the chart created, and refuses one it did not", async () => { + const managed = keycloak("managed"); + const run = jobIO(new MemoryObjects(), managed.fetcher); + await runKeycloakJob(installation(), manifestFixture(), run.io, converge().functions); + expect(run.lines).toContain("keycloak: realm libredb exists"); + expect(managed.requests.some((request) => request.startsWith("POST") && request.endsWith("/admin/realms"))).toBe(false); + const foreign = keycloak("foreign"); + await expect(runKeycloakJob(installation(), manifestFixture(), jobIO(new MemoryObjects(), foreign.fetcher).io, converge().functions)).rejects.toThrow( + MANAGED_ATTRIBUTE, + ); + }); + + test("an external Keycloak with provisioning installer converges with the site's admin credential and no realm creation", async () => { + const run = jobIO(new MemoryObjects(), (async () => new Response("", { status: 200 })) as unknown as typeof fetch); + const step = converge(); + await runKeycloakJob(installation(INSTALLER_KEYCLOAK), manifestFixture(), run.io, step.functions); + expect(step.seen.target?.baseUrl).toBe("https://kc-admin.corp.example"); + expect(step.seen.target?.credentials).toEqual({ realm: "master", clientId: "corp-admin", clientSecret: "corp-admin-secret" }); + expect(run.written.size).toBe(0); + expect(step.seen.applied).toBeGreaterThan(0); + expect(run.lines.at(-1)).toBe(`keycloak: applied ${step.seen.applied} actions`); + }); +}); + +describe("adminTarget", () => { + test("refuses provisioning manual and a missing admin credential", () => { + const sources = { readFile: () => undefined, env: {}, redactor: new Redactor() }; + expect(() => adminTarget(installation(MANUAL_KEYCLOAK), sources)).toThrow("provisioning is manual"); + expect(() => adminTarget(installation(INSTALLER_KEYCLOAK), sources)).toThrow("does not exist"); + }); + + test("refuses a managed Keycloak whose bootstrap secret is not mounted", () => { + expect(() => adminTarget(installation(), { readFile: () => undefined, env: {}, redactor: new Redactor() })).toThrow( + `Secret ${K8S_NAMES.keycloakConfig} holds no ${STORE_KEYS.keycloakBootstrapSecret}`, + ); + }); + + test("an admin user signs in through admin-cli and reaches the Admin API at the base URL without an adminUrl", () => { + const config = installation({ + keycloak: { + mode: "external", + realm: "corp", + external: { baseUrl: "https://auth.libredb.test", adminUser: "admin", adminPasswordEnv: "CORP_ADMIN_PASSWORD" }, + }, + }); + const target = adminTarget(config, { readFile, env: { CORP_ADMIN_PASSWORD: "pw" }, redactor: new Redactor() }); + expect(target).toEqual({ + baseUrl: "https://auth.libredb.test", + credentials: { realm: "master", clientId: "admin-cli", username: "admin", password: "pw" }, + }); + }); +}); + +describe("ensureManagedRealm", () => { + test("creates the realm with the installer's managed settings", async () => { + const writes: unknown[] = []; + const admin = { find: async () => undefined, write: async (...args: unknown[]) => writes.push(args) } as never; + expect(await ensureManagedRealm(admin, "libredb")).toBe("created"); + expect(JSON.stringify(writes)).toContain(`"${MANAGED_ATTRIBUTE}":"true"`); + }); +}); + +describe("waitForKeycloak", () => { + test("waits while Keycloak refuses connections, then returns once it answers", async () => { + let attempts = 0; + const lines: string[] = []; + const slept: number[] = []; + const fetcher = (async () => { + attempts += 1; + if (attempts < 3) throw new TypeError("fetch failed"); + return new Response("", { status: 404 }); + }) as unknown as typeof fetch; + await waitForKeycloak("http://keycloak:8080", { fetch: fetcher, log: (line) => lines.push(line), sleep: async (ms) => void slept.push(ms), now: () => 0 }); + expect(attempts).toBe(3); + expect(slept).toEqual([3000, 3000]); + expect(lines).toEqual(["keycloak: waiting for http://keycloak:8080 to answer", "keycloak: http://keycloak:8080 answers"]); + }); + + test("fails with the last connection error once the timeout passed", async () => { + let clock = 0; + const fetcher = (async () => { + throw new TypeError("fetch failed"); + }) as unknown as typeof fetch; + const waiting = waitForKeycloak("http://keycloak:8080", { + fetch: fetcher, + log: () => {}, + sleep: async (ms) => void (clock += ms), + now: () => clock, + timeoutMs: 10_000, + intervalMs: 4_000, + }); + await expect(waiting).rejects.toThrow("Keycloak at http://keycloak:8080 did not answer within 10 s: fetch failed"); + }); +}); From fc8e5d3e1f8ea470bbf4593070be542c53b0e5d2 Mon Sep 17 00:00:00 2001 From: Abdullah Kaya Date: Fri, 23 Oct 2026 12:43:00 +0300 Subject: [PATCH 2/5] fix(chart): sign in behind ingress-nginx and retire seal keys under network policies Platform's sign-in callback sets its session cookie in chunks that hold the Keycloak ID and refresh tokens, more response header than the 4k that ingress-nginx buffers by default, so the first sign-in of a chart behind ingress-nginx ended in a 502 (upstream sent too big header). The Ingresses now carry nginx.ingress.kubernetes.io/proxy-buffer-size: 16k, which other controllers ignore. The browser suite's first test passed on that 502, because the callback URL is not /login; it now asks /api/auth/session for the signed-in admin, as the Docker suite does. The seal-envelopes and seal-canary init containers ran their script with one psql call. A network policy controller such as k3s' kube-router refuses a new pod's first connection until it holds the pod's address, so every bootstrap pod of a retirement failed at once and the upgrade hung until its timeout. The containers now wait up to 30 s for a connection, and still run the script once, so a refusal stays the job's failure. Found on k3s v1.36 behind Dokploy (Traefik) and CapRover (ingress-nginx); run-e2e.sh, run-rotation.sh, run-restore.sh and run-argocd.sh pass there with these changes. --- .../libredb-platform/templates/_helpers.tpl | 5 ++ charts/libredb-platform/values.yaml | 6 ++- installer/e2e/kubernetes/helm-install.k8s.ts | 6 +++ installer/test/unit/kubernetes/chart.test.ts | 47 ++++++++++++++++++- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/charts/libredb-platform/templates/_helpers.tpl b/charts/libredb-platform/templates/_helpers.tpl index 1ae1d8d..3884cb0 100644 --- a/charts/libredb-platform/templates/_helpers.tpl +++ b/charts/libredb-platform/templates/_helpers.tpl @@ -153,6 +153,11 @@ from Secret libredb-platform-migrate-env in PGPASSWORD, never on a command line: address="${rest#*@}" address="${address%%/*}" conninfo="host=${address%:*} port=${address##*:} dbname=libredb user=libredb_platform sslmode=verify-full sslrootcert=/etc/libredb/ca/ca.crt connect_timeout=10" + # A network policy controller such as k3s' kube-router may refuse a new pod's first connections until it holds + # the pod's address, so the container waits up to 30 s for one; the script itself runs once, and its failure + # is the job's. + tries=0 + until psql "$conninfo" -X -q -A -t -c 'SELECT 1' >/dev/null 2>&1 || [ "$tries" -ge 30 ]; do tries=$((tries + 1)); sleep 1; done psql "$conninfo" -X -q -A -t -v ON_ERROR_STOP=1 -f {{ .file }}{{ with .output }} > {{ . }}{{ end }} env: - name: DATABASE_URL diff --git a/charts/libredb-platform/values.yaml b/charts/libredb-platform/values.yaml index 6480cfc..73d6e8e 100644 --- a/charts/libredb-platform/values.yaml +++ b/charts/libredb-platform/values.yaml @@ -93,7 +93,11 @@ ingress: # -- The Ingress of Platform and of a managed Keycloak. Studio's Ingress is studio.ingress. enabled: true className: "" - annotations: {} + annotations: + # Platform's sign-in callback sets its session cookie in chunks that hold the Keycloak ID and refresh tokens, + # more response header than ingress-nginx buffers by default (4k), which then answers the callback 502. Other + # controllers ignore the annotation; an nginx-based one of another vendor needs its own setting of 16k. + nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" postgresql: # -- managed: the chart runs PostgreSQL with verified TLS from the internal CA and provisions it, as the installer diff --git a/installer/e2e/kubernetes/helm-install.k8s.ts b/installer/e2e/kubernetes/helm-install.k8s.ts index 484cd09..00c2bbb 100644 --- a/installer/e2e/kubernetes/helm-install.k8s.ts +++ b/installer/e2e/kubernetes/helm-install.k8s.ts @@ -117,6 +117,12 @@ test("the first admin signs in to Platform for the first time with a new passwor await expect(platform).toHaveURL((url) => url.origin === PLATFORM_URL && url.pathname !== "/login"); await platform.waitForLoadState("networkidle"); await shot(platform, "01-platform-after-first-login"); + // A callback that fails at the ingress, such as a 502 for its response headers, also leaves /login behind, so the + // session itself is the proof, asked from the page as the Docker suite asks it. + const session = await platform.evaluate( + async () => (await fetch("/api/auth/session")).json() as Promise<{ user?: { email?: string } } | null>, + ); + expect(session?.user?.email).toBe(firstAdmin.email); }); test("the license page shows the license the chart installed as active", async () => { diff --git a/installer/test/unit/kubernetes/chart.test.ts b/installer/test/unit/kubernetes/chart.test.ts index 46694fc..00aead2 100644 --- a/installer/test/unit/kubernetes/chart.test.ts +++ b/installer/test/unit/kubernetes/chart.test.ts @@ -4,7 +4,8 @@ // bootstrap Role against every object the bootstrap job writes, and the pod security of every workload. import { beforeAll, describe, expect, test } from "bun:test"; import { execFileSync } from "node:child_process"; -import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { parse, parseAllDocuments } from "yaml"; import { parseInstallation } from "../../../src/config/schema"; @@ -209,6 +210,44 @@ describe("the seal key and AUTH_SECRET rotations", () => { } }); + test("the retirement's SQL waits for a connection, which a new pod's network policy may refuse at first, and runs once", () => { + const docs = render([`platform.sealKeys.activeKid=${NEW}`, `platform.sealKeys.retire[0]=${OLD}`]); + const work = mkdtempSync(join(tmpdir(), "libredb-seal-sql-")); + // A psql that refuses the first two connections, as kube-router does until it has the pod's address, and records + // each script it runs; SCRIPT_EXIT is what the script run exits with. + writeFileSync( + join(work, "psql"), + [ + "#!/bin/sh", + `n=$(cat "${work}/tries" 2>/dev/null || echo 0); echo $((n + 1)) > "${work}/tries"`, + '[ "$n" -ge 2 ] || exit 2', + `case " $* " in *" -f "*) echo run >> "${work}/runs"; exit "$SCRIPT_EXIT" ;; esac`, + ].join("\n"), + { mode: 0o755 }, + ); + const run = (pod: Doc, scriptExit: number) => { + rmSync(join(work, "tries"), { force: true }); + rmSync(join(work, "runs"), { force: true }); + const script = (podOf(pod).initContainers[0].command[2] as string).replaceAll(SEAL_ENVELOPES_FILE, join(work, "out")); + const result = Bun.spawnSync(["/bin/sh", "-c", script], { + env: { PATH: `${work}:/usr/bin:/bin`, SCRIPT_EXIT: String(scriptExit), DATABASE_URL: "postgresql://libredb_platform:pw@libredb-postgres:5432/libredb?schema=platform" }, + }); + const runs = existsSync(join(work, "runs")) ? readFileSync(join(work, "runs"), "utf8").trim().split("\n").length : 0; + return { code: result.exitCode, runs }; + }; + try { + for (const job of ["libredb-bootstrap", "libredb-restart"]) { + const pod = find(docs, "Job", job); + expect(run(pod, 0), job).toEqual({ code: 0, runs: 1 }); + // A failing script is the refusal itself, so it is never run again. + expect(run(pod, 3), job).toEqual({ code: 3, runs: 1 }); + } + } finally { + rmSync(work, { recursive: true, force: true }); + } + // Each of the four runs waits 1 s per refused connection. + }, 30_000); + test("a kid that is no seal key id fails the render, naming the value", () => { expect(failure(["platform.sealKeys.retire[0]=x'; DROP TABLE y;--"])).toContain("platform.sealKeys.retire[0]"); expect(failure(["platform.sealKeys.activeKid=ABCDEF0123456789"])).toContain("platform.sealKeys.activeKid"); @@ -340,6 +379,12 @@ describe("the workloads", () => { expect(ingress.spec.rules[0].http.paths[0].backend.service.name).toBe("keycloak-edge"); }); + test("Platform's Ingress asks ingress-nginx for a 16k header buffer, which Platform's sign-in callback needs", () => { + const annotation = "nginx.ingress.kubernetes.io/proxy-buffer-size"; + expect(find(docs(), "Ingress", "libredb-platform").metadata.annotations?.[annotation]).toBe("16k"); + expect(find(render(["ingress.annotations.team=dba"]), "Ingress", "libredb-platform").metadata.annotations).toEqual({ [annotation]: "16k", team: "dba" }); + }); + test("a job's name follows the revision and what it runs: the same values keep it, a new installation document renames it", () => { const names = (sets: string[]) => render(sets).filter((doc) => doc.kind === "Job").map((doc) => doc.metadata.name).sort(); expect(names([])).toEqual(names([])); From b11f36508731e69951871fe818eb60e9b3abae49 Mon Sep 17 00:00:00 2001 From: Abdullah Kaya Date: Fri, 23 Oct 2026 12:43:00 +0300 Subject: [PATCH 3/5] docs(kubernetes): add the Studio chart repository before helm dependency build helm dependency build takes the Studio chart that Chart.lock pins only from a repository Helm knows, so on a fresh machine the guide's first command stopped with "no repository definition for https://libredb.org/libredb-studio/". CI never ran it, because it copies the chart that embed downloaded. The guide, the chart README and run-e2e.sh now run helm repo add first. The guide also names ingress-nginx and the 16 KB of response headers an ingress controller must accept, the chart README lists the annotation's default, and the section on an install without network access gets its missing heading. --- charts/libredb-platform/README.md | 3 ++- charts/libredb-platform/tests/e2e/run-e2e.sh | 6 +++++- docs/deploy/install-kubernetes.md | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/charts/libredb-platform/README.md b/charts/libredb-platform/README.md index f10b291..929bdcf 100644 --- a/charts/libredb-platform/README.md +++ b/charts/libredb-platform/README.md @@ -10,6 +10,7 @@ It renders the values as an installation configuration of `mode: kubernetes`, wh ## Install ```sh +helm repo add libredb-studio https://libredb.org/libredb-studio/ helm dependency build charts/libredb-platform helm install libredb charts/libredb-platform -n libredb --create-namespace \ -f libredb-values.yaml --set-file platform.license.value=acme.license \ @@ -50,7 +51,7 @@ When one of them changes, the restart job restarts the workloads that read it, o | `tls.mode` | `generated` | `generated`: the internal CA issues the ingress certificates; `secrets`: your TLS Secrets | | `tls.secrets.platform`, `studio`, `auth` | | With `tls.mode: secrets`, the TLS Secret of each host (`auth` only with a managed Keycloak); repeat `studio` as `studio.ingress.tls[0].secretName` | | `tls.caBundle` | | With `tls.mode: secrets`, the PEM of the CA that issued them (required) | -| `ingress.enabled`, `className`, `annotations` | `true` | The Ingress of Platform and of a managed Keycloak | +| `ingress.enabled`, `className`, `annotations` | `true`, `""`, `nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"` | The Ingress of Platform and of a managed Keycloak; the annotation gives ingress-nginx room for the response headers of Platform's sign-in callback, and other controllers ignore it | | `postgresql.mode` | `managed` | `managed` or `external` | | `postgresql.managed.image`, `persistence.size`, `persistence.storageClass`, `resources`, `nodeSelector`, `tolerations`, `affinity` | the release's image, `10Gi` | The managed server | | `postgresql.external.host`, `port`, `caBundle` | `""`, `5432` | The external server, provisioned once by a DBA with Secret `libredb-postgres-dba-script` | diff --git a/charts/libredb-platform/tests/e2e/run-e2e.sh b/charts/libredb-platform/tests/e2e/run-e2e.sh index 9813b5b..fce2dd3 100755 --- a/charts/libredb-platform/tests/e2e/run-e2e.sh +++ b/charts/libredb-platform/tests/e2e/run-e2e.sh @@ -50,7 +50,11 @@ else fi step "chart dependencies" -[ -f "$chart/charts/libredb-studio-0.1.100.tgz" ] || helm dependency build "$chart" +if [ ! -f "$chart/charts/libredb-studio-0.1.100.tgz" ]; then + # helm dependency build takes Chart.lock's Studio chart only from a repository Helm knows. + helm repo add libredb-studio https://libredb.org/libredb-studio/ >/dev/null 2>&1 || true + helm dependency build "$chart" +fi step "install into a namespace that enforces the restricted Pod Security Standard" kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - >/dev/null diff --git a/docs/deploy/install-kubernetes.md b/docs/deploy/install-kubernetes.md index 6b83865..e2ab6bc 100644 --- a/docs/deploy/install-kubernetes.md +++ b/docs/deploy/install-kubernetes.md @@ -8,7 +8,8 @@ The [chart README](../../charts/libredb-platform/README.md) lists every value. ## What you need - Kubernetes 1.28 or newer, with a default StorageClass for the PostgreSQL volume and Platform's uploads. -- An ingress controller with an IngressClass, such as Traefik or HAProxy, which terminates TLS for the three hosts. +- An ingress controller with an IngressClass, such as Traefik, HAProxy or ingress-nginx, which terminates TLS for the three hosts and accepts response headers of 16 KB: Platform's sign-in callback sets its session cookie in chunks, more than the 4 KB that nginx buffers by default. + The chart sets `nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"` on its Ingresses for ingress-nginx; another nginx-based controller needs its own setting, or the first sign-in ends in a 502 from the controller. - Helm 3. - At least one x86_64 (amd64) node: the Platform image is built for linux/amd64 alone, so the chart schedules Platform and the jobs that run its image with `kubernetes.io/arch: amd64`; Studio, Keycloak and PostgreSQL also run on arm64 nodes. - Three DNS names, for Platform, Studio and Keycloak, such as `libredb.acme.example`, `studio.acme.example` and `sso.acme.example`, which resolve to the ingress controller from the users' browsers **and from the cluster's pods**: Platform and Studio call Keycloak's issuer URL `https:///realms/libredb` from inside the cluster. @@ -47,12 +48,14 @@ Set `ingress.className` and `studio.ingress.className` when the cluster has no d ## Install ```sh +helm repo add libredb-studio https://libredb.org/libredb-studio/ helm dependency build charts/libredb-platform helm install libredb charts/libredb-platform -n libredb --create-namespace \ -f libredb-values.yaml --set-file platform.license.value=acme.license \ --wait --wait-for-jobs --timeout 10m ``` +`helm dependency build` takes the Studio chart that `Chart.lock` pins only from a repository Helm knows, so without the `helm repo add` it stops with `no repository definition for https://libredb.org/libredb-studio/`. Instead of `--set-file`, `platform.license.existingSecret` names a Secret that holds the token under the key `license`. The install runs in this order, and `--wait --wait-for-jobs` returns once every step is done: @@ -318,6 +321,7 @@ The chart installs on OpenShift 4 with the default `restricted-v2` SCC and names - OpenShift turns each Ingress into a Route that terminates TLS at the router with the Ingress's Secret, so `tls.mode: generated` works unchanged; the hosts are names under the cluster's apps domain, or names your DNS points at the router. - The project template may add NetworkPolicies of its own, such as an `allow-same-namespace` policy, and policies add up: there, every pod of the namespace reaches PostgreSQL and Keycloak's Admin API, so give the installation a namespace of its own. +## An install without network access `helm dependency build` fetches the pinned Studio chart from `https://libredb.org/libredb-studio/`. For a site without that access, run `helm package --dependency-update charts/libredb-platform` on a machine that has it and install the archive, which holds the dependency, so Helm on the site needs no repository. From 2c594adc4e0370f9efb044c2a1996ab264af73e1 Mon Sep 17 00:00:00 2001 From: Abdullah Kaya Date: Fri, 23 Oct 2026 12:43:00 +0300 Subject: [PATCH 4/5] fix(installer): document and verify tls.mode external beside Dokploy or CapRover Both PaaS hold ports 80 and 443, so LibreDB installs there in tls.mode external behind their proxy. Tested on Dokploy 0.30.10 and CapRover 1.15.4, which found: - An nginx with its defaults, such as CapRover's, answers Platform's sign-in callback 502 while every verify check passes; the README now asks for 16 KB of response headers and gives the nginx setting. - A DOCKER-USER rule on port 8080 alone also drops the apps' calls to Keycloak on its port 8080; the README gives a rule that matches the external interface. - The proxy arrives from the gateway of libredb_edge, which Docker picks only when the first apply creates the network; the README says how to read it, and studio-oidc-chain now names trustedProxies when Studio sends its callback with http:. - Dokploy's daily cleanup and CapRover after every deploy prune stopped containers, after which "docker compose start studio" starts nothing and exits 0, and Dokploy's cleanup of unused volumes deletes their volumes. The kill switch runbook now starts Studio with up -d, which the runbooks drill checks after pruning the stopped container, and the README describes both cleanups. --- installer/README.md | 16 ++++++++++++++++ installer/docs/runbooks/break-glass.md | 4 +++- installer/e2e/runbooks.spec.ts | 5 ++++- installer/src/verify/oidc-chain.ts | 12 +++++++++--- installer/test/unit/verify/oidc-chain.test.ts | 7 +++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/installer/README.md b/installer/README.md index 81a7731..c4002cf 100644 --- a/installer/README.md +++ b/installer/README.md @@ -150,14 +150,30 @@ The installer never renews a provided certificate: replace the files and run `ap `tls.mode: external` puts the edge behind the customer's TLS proxy. The edge serves plain HTTP on `tls.external.httpPort` (default 8080), one site per public hostname, and the upstream proxy terminates TLS on 443 of each public hostname and forwards to that port with `X-Forwarded-Proto` and `X-Forwarded-Host`. +The proxy must accept response headers of 16 KB: Platform's sign-in callback sets its session cookie in chunks, which hold the Keycloak ID and refresh tokens, beside the state and PKCE cookies it clears, more than the 4 KB that nginx buffers by default on x86_64, so an nginx with its defaults answers that callback 502 (`upstream sent too big header`) while every check of `verify` passes. +With nginx, such as the proxy of CapRover, set `proxy_buffer_size 16k;` and `proxy_buffers 8 16k;` in the location that forwards to the edge; Traefik, as Dokploy runs it, needs no setting. `tls.external.trustedProxies` is required and has no default: it lists the upstream proxy's addresses as the edge sees them, as CIDR ranges such as `192.0.2.10/32`, and the edge passes `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` on only from them, so `validate` refuses `tls.mode: external` without it with exit 2. The edge sees a proxy on another host at that host's own address when no NAT lies between them, and a proxy container on the network `libredb_edge` at its address there, while a proxy on the Docker host that connects through `127.0.0.1` arrives from the gateway of `libredb_edge`, the address that containers of other networks arrive from as well when they connect to the published port. +Docker picks that gateway when the first `apply` creates the network, such as `172.19.0.1` on a host whose bridge networks hold `172.17.0.0/16` and `172.18.0.0/16`, as the default bridge and the `docker_gwbridge` of a Swarm such as Dokploy's or CapRover's do; `docker network inspect libredb_edge -f '{{(index .IPAM.Config 0).Gateway}}'` prints it, and when it differs from `trustedProxies`, `verify` fails `studio-oidc-chain` with a `redirect_uri` of `http://`, so correct the value and run `apply` again. `private_ranges` is accepted when written out, but it trusts every client on a private network. The edge's HTTP port must be reachable only from the upstream proxy, because a client that reaches it directly bypasses the proxy's TLS: restrict it with a host firewall rule, which with Docker's iptables backend belongs in the `DOCKER-USER` chain, because Docker forwards the traffic of a published port past the `INPUT` chain. +Match the host's external interface in that rule, such as `iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 -j DROP` for a proxy on the Docker host: Docker passes the traffic between containers through `DOCKER-USER` as well, so a rule on the port alone also drops Platform's and Studio's calls to Keycloak on its port 8080, and `verify` then fails `platform-admin-api`, `platform-signin-redirect`, `studio-oidc-chain` and `oidc-discovery-in-network`. The edge then holds no hostname alias, so the containers reach the public issuer and app URLs through the upstream proxy: every public hostname must resolve from inside the network `libredb_edge` to the proxy, and the proxy must present a certificate for it that validates against `tls.caBundleFile`, which is required. `preflight` checks both from a one-shot container of the Platform image on `libredb_edge`, or on Docker's default bridge before the first `apply` has created that network, and refuses with exit 3 naming each host that does not resolve or present a valid chain. `verify`'s `edge-tls` then connects to each public hostname on 443 by its name, through the upstream proxy. +#### Beside Dokploy or CapRover + +Dokploy and CapRover hold ports 80 and 443 of their host, where `preflight` refuses the default `tls.mode: generated` with exit 3 naming their proxy container, so LibreDB installs on such a host in `tls.mode: external` behind their proxy: + +- Dokploy: a file under `/etc/dokploy/traefik/dynamic` with an HTTP router for the three hostnames on entry point `websecure`, `tls: {}`, the site's certificate in a `tls.certificates` entry, and a service to `http://:8080`. +- CapRover: an app with the three hostnames as custom domains whose custom nginx configuration listens on 443 with the site's certificate, which CapRover's own HTTPS gives only from Let's Encrypt, proxies to `http://:8080` and sets `proxy_buffer_size 16k;` and `proxy_buffers 8 16k;`. +- `trustedProxies` holds the gateway of `libredb_edge`, which both proxies arrive from. + +Both clean up Docker on their own, which removes the containers of a LibreDB service that is stopped at that moment, such as Studio after the kill switch of [break-glass.md](docs/runbooks/break-glass.md) or the apps during a [restore](docs/runbooks/restore.md): Dokploy's daily Docker cleanup, on by default, runs `docker container prune`, `docker image prune --all` and `docker system prune --all`, and CapRover runs `docker container prune` after every deploy of one of its apps. +`docker compose up -d` and `apply` create such a container again, with its volume. +Dokploy's cleanup of unused volumes (`docker volume prune --all`), which its daily cleanup leaves out, also deletes the volume of a removed container, such as `libredb_platform-uploads` with every support attachment, so never run it while a LibreDB service is stopped. + The managed PostgreSQL leaf comes from the internal CA in every TLS mode. `tls export-ca [-o [--force]]` writes the internal CA certificate, without its key, for the trust stores of database clients and, in `tls.mode: generated`, of browsers: diff --git a/installer/docs/runbooks/break-glass.md b/installer/docs/runbooks/break-glass.md index baa4898..1871269 100644 --- a/installer/docs/runbooks/break-glass.md +++ b/installer/docs/runbooks/break-glass.md @@ -59,9 +59,11 @@ Studio's public URL answers 502 from the edge while it is stopped. Start it again when the cause is dealt with; a full `apply` starts it as well: ```sh -docker compose -p libredb -f /opt/libredb/compose.yaml start studio +docker compose -p libredb -f /opt/libredb/compose.yaml up -d studio ``` +`up -d` also creates the container again when something removed the stopped one, as `docker container prune` does, which a PaaS on the same host may run on its own, such as Dokploy's daily Docker cleanup or CapRover after every deploy; `start` then starts nothing and still exits 0. + ## PostgreSQL is down Studio keeps serving from its last governance snapshot until that snapshot is older than `GOVERNANCE_MAX_STALENESS_MS` (120000 ms by default), and then stops all data access by design, because it can no longer see revocations. diff --git a/installer/e2e/runbooks.spec.ts b/installer/e2e/runbooks.spec.ts index 5ca47c0..35b8d6c 100644 --- a/installer/e2e/runbooks.spec.ts +++ b/installer/e2e/runbooks.spec.ts @@ -461,8 +461,11 @@ test("break-glass: the kill switch stops Studio, start brings it back, and statu expectSuccess(runBlock(runbook, 3, "stop studio")); expect(await ready()).toBe(502); + // A PaaS on the host, such as Dokploy or CapRover, may prune the stopped container; the start block brings Studio + // back all the same. + asRootOk("docker container prune --force --filter label=com.docker.compose.project=libredb --filter label=com.docker.compose.service=studio"); - expectSuccess(runBlock(runbook, 4, "start studio")); + expectSuccess(runBlock(runbook, 4, "up -d studio")); await expect.poll(ready, { timeout: 120_000, intervals: [2_000] }).toBe(200); const status = expectSuccess(runBlock(runbook, 5, "status")); diff --git a/installer/src/verify/oidc-chain.ts b/installer/src/verify/oidc-chain.ts index 337f192..c4afe2f 100644 --- a/installer/src/verify/oidc-chain.ts +++ b/installer/src/verify/oidc-chain.ts @@ -67,6 +67,12 @@ const chainResult = z }) .strict(); +/** Why Studio sends its callback with http: where the public URL is https:. */ +const PLAIN_HTTP_HINT = + "Studio saw plain HTTP, so the edge did not pass X-Forwarded-Proto on; in tls.mode external, " + + "tls.external.trustedProxies must hold the address the upstream proxy arrives from, " + + "which for a proxy on the Docker host is the gateway of the network libredb_edge"; + /** What a redirect to /login?error= of Studio's OIDC login route means. */ function loginErrorMeaning(code: string, issuer: string): string { if (code === "oidc_config") return "Studio's OIDC configuration is invalid"; @@ -120,9 +126,9 @@ export const studioOidcChainCheck: Check = { } const redirectUri = location.searchParams.get("redirect_uri"); if (redirectUri !== oidc.studioRedirectUri) { - return failed( - `the authorization request sends redirect_uri ${redirectUri}, and Studio's callback is ${oidc.studioRedirectUri}`, - ); + const cause = `the authorization request sends redirect_uri ${redirectUri}, and Studio's callback is ${oidc.studioRedirectUri}`; + // Studio builds its callback from X-Forwarded-Proto, which the edge drops from a proxy it does not trust. + return failed(redirectUri === oidc.studioRedirectUri.replace(/^https:/, "http:") ? `${cause}: ${PLAIN_HTTP_HINT}` : cause); } const prompt = location.searchParams.get("prompt"); if (prompt !== null) { diff --git a/installer/test/unit/verify/oidc-chain.test.ts b/installer/test/unit/verify/oidc-chain.test.ts index c695afc..7303325 100644 --- a/installer/test/unit/verify/oidc-chain.test.ts +++ b/installer/test/unit/verify/oidc-chain.test.ts @@ -125,6 +125,13 @@ describe("studio-oidc-chain", () => { chain(authorize({ ...good, redirect_uri: "https://localhost:3000/api/auth/oidc/callback" })), `the authorization request sends redirect_uri https://localhost:3000/api/auth/oidc/callback, and Studio's callback is ${REDIRECT_URI}`, ], + [ + chain(authorize({ ...good, redirect_uri: "http://studio.example.com/api/auth/oidc/callback" })), + `the authorization request sends redirect_uri http://studio.example.com/api/auth/oidc/callback, and Studio's callback is ${REDIRECT_URI}: ` + + "Studio saw plain HTTP, so the edge did not pass X-Forwarded-Proto on; in tls.mode external, " + + "tls.external.trustedProxies must hold the address the upstream proxy arrives from, " + + "which for a proxy on the Docker host is the gateway of the network libredb_edge", + ], [ chain(authorize({ ...good, prompt: "login" }), { authStatus: 200 }), "the authorization request sends prompt=login, and OIDC_PROMPT=omit sends none", From b1717a55836d61abfa190c935fd0802399084aec Mon Sep 17 00:00:00 2001 From: cevheri Date: Sat, 24 Oct 2026 01:26:28 +0300 Subject: [PATCH 5/5] build(chart): rebuild the jobs bundle on main's installer code Main changed the installer's Keycloak and platform.env code (#34), so the committed bundle no longer matched; regenerated with bun run build:helm-jobs. --- .../files/libredb-jobs.cjs.gz | Bin 173137 -> 173873 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/charts/libredb-platform/files/libredb-jobs.cjs.gz b/charts/libredb-platform/files/libredb-jobs.cjs.gz index 6fb11e312313908e356356207feb510c48c5e512..c65b44b7dd75837ce80e9eb8d0b762e927ca4da4 100644 GIT binary patch delta 171624 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 delta 170943 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�Ay=> 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
b)p^>}DiPN+yGNE>lbpZCJk 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%0D`-X?_gq0U&z(dW*=JDl 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